Merge branch 'develop' into feature/farming_bugfixes_and_magic_word
This commit is contained in:
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Babushka.scripts.CSharp.Common.Services;
|
||||
using Godot;
|
||||
|
||||
@@ -11,19 +12,21 @@ 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 CanvasItem? _spriteToOutline; // keep to not break old usages. TODO: remove later
|
||||
[Export] private CanvasItem[] _spritesToOutline;
|
||||
[Export] private bool _showLabel = true;
|
||||
[Export] private int _id = -1; // TODO: remove
|
||||
|
||||
private Material _backupMaterial;
|
||||
private Material[] _backupMaterials;
|
||||
|
||||
[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 void SetActiveInverse(bool active)
|
||||
@@ -33,52 +36,66 @@ public partial class InteractionArea2D : Node2D
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
if (_useSprite && _useOutline)
|
||||
if (_useOutline)
|
||||
{
|
||||
try
|
||||
{
|
||||
_backupMaterial = _spriteToOutline.Material;
|
||||
// support old implementations of the script. If the sprite to outline is set, add it to the array
|
||||
if (_spriteToOutline != null)
|
||||
{
|
||||
Array.Resize(ref _spritesToOutline, _spritesToOutline.Length + 1);
|
||||
_spritesToOutline[^1] = _spriteToOutline;
|
||||
}
|
||||
|
||||
_backupMaterials = _spritesToOutline.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 || !InputService.Instance.InputEnabled)
|
||||
return;
|
||||
|
||||
if(_showLabel)
|
||||
|
||||
if (_showLabel)
|
||||
_label.Show();
|
||||
|
||||
if (!_useSprite || !_useOutline)
|
||||
|
||||
if (!_useOutline)
|
||||
return;
|
||||
|
||||
_spriteToOutline.Material = _outlineMaterial;
|
||||
|
||||
foreach (var sprite in _spritesToOutline)
|
||||
{
|
||||
sprite.Material = _outlineMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPlayerExited(Node2D player)
|
||||
{
|
||||
if (!_active)
|
||||
return;
|
||||
|
||||
|
||||
_label.Hide();
|
||||
|
||||
if (!_useSprite || !_useOutline)
|
||||
if (!_useOutline)
|
||||
return;
|
||||
|
||||
_spriteToOutline.Material = _backupMaterial;
|
||||
|
||||
for (var i = 0; i < _spritesToOutline.Length; i++)
|
||||
{
|
||||
var sprite = _spritesToOutline[i];
|
||||
sprite.Material = _backupMaterials[i];
|
||||
}
|
||||
}
|
||||
|
||||
public override void _Input(InputEvent @event)
|
||||
{
|
||||
if (!_active || !InputService.Instance.InputEnabled)
|
||||
if (!_active || !InputService.Instance.InputEnabled)
|
||||
return;
|
||||
|
||||
|
||||
if (@event.IsAction("interact") && @event.IsPressed())
|
||||
{
|
||||
TryInteract();
|
||||
@@ -95,10 +112,16 @@ public partial class InteractionArea2D : Node2D
|
||||
if (_area.HasOverlappingAreas())
|
||||
{
|
||||
_label.Hide();
|
||||
|
||||
if (_useSprite && _useOutline)
|
||||
_spriteToOutline.Material = _backupMaterial;
|
||||
|
||||
|
||||
if (_useOutline)
|
||||
{
|
||||
for (var i = 0; i < _spritesToOutline.Length; i++)
|
||||
{
|
||||
var sprite = _spritesToOutline[i];
|
||||
sprite.Material = _backupMaterials[i];
|
||||
}
|
||||
}
|
||||
|
||||
EmitSignal(SignalName.InteractedTool, _id);
|
||||
EmitSignal(SignalName.Interacted);
|
||||
}
|
||||
@@ -107,7 +130,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,25 @@
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public partial class ActionAnimationController : Node
|
||||
{
|
||||
#region Shortcuts
|
||||
|
||||
private FightWorld.FightHappeningData HappeningData => FightWorld.Instance.fightHappeningData ?? throw new NoFightHappeningException();
|
||||
|
||||
#endregion
|
||||
|
||||
[Export] private AllFightersVisual _allFightersVisual = null!;
|
||||
|
||||
|
||||
public void StateEnter()
|
||||
{
|
||||
_ = HappeningData.actionStaging!.AnimateAction(_allFightersVisual);
|
||||
}
|
||||
|
||||
public void StateExit()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtf4ejct4m682
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight.ActionDetails;
|
||||
|
||||
public class MinigameActionDetail : FighterAction.FighterActionDetail
|
||||
{
|
||||
// settings
|
||||
|
||||
|
||||
// result
|
||||
public List<int>? damageHits = null;
|
||||
|
||||
public MinigameActionDetail()
|
||||
{
|
||||
}
|
||||
|
||||
public override bool DetailComplete()
|
||||
{
|
||||
return damageHits != null;
|
||||
}
|
||||
|
||||
public void ResetResult()
|
||||
{
|
||||
damageHits = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtn4la0ycl5wx
|
||||
@@ -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,65 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
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 MinigameActionDetail minigameDetail = new();
|
||||
|
||||
public override Variant<float, Func<bool>> GetAnimationEnd()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public override bool NextDetail()
|
||||
{
|
||||
return !targetSelect.DetailComplete() || !minigameDetail.DetailComplete();
|
||||
}
|
||||
|
||||
public override FighterActionDetail CurrentDetail()
|
||||
{
|
||||
return targetSelect.DetailComplete() ? minigameDetail : targetSelect;
|
||||
}
|
||||
|
||||
public override AllyActionButton BindToActionButton()
|
||||
{
|
||||
return AllyActionButton.Attack;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
targetSelect.ResetResult();
|
||||
minigameDetail.ResetResult();
|
||||
}
|
||||
|
||||
public override void ExecuteAction()
|
||||
{
|
||||
var totalDamage = minigameDetail.damageHits!.Sum(dh => dh);
|
||||
targetSelect.GetTarget().AddHealth(-totalDamage);
|
||||
}
|
||||
|
||||
public override async Task AnimateAction(AllFightersVisual allFightersVisual)
|
||||
{
|
||||
var currentFighter = HappeningData.fighterTurn.Current;
|
||||
var targetFighter = targetSelect.GetTarget();
|
||||
|
||||
var currentFighterVisual = allFightersVisual.GetVisualForFighter(currentFighter);
|
||||
var targetFighterVisual = allFightersVisual.GetVisualForFighter(targetFighter);
|
||||
|
||||
await currentFighterVisual.AnimatePosToTarget(targetFighterVisual);
|
||||
_ = targetFighterVisual.AnimateHit();
|
||||
await currentFighterVisual.AnimatePosToBase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8c4t80bqsja5
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Babushka.scripts.CSharp.Common.Util;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight.Actions;
|
||||
|
||||
public class BlobAttackAction : FighterAction
|
||||
{
|
||||
public override Variant<float, Func<bool>> GetAnimationEnd()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public override bool NextDetail()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void ExecuteAction()
|
||||
{
|
||||
FightWorld.Instance.allyFighters.vesnaFighter.AddHealth(-3);
|
||||
}
|
||||
|
||||
public override async Task AnimateAction(AllFightersVisual allFightersVisual)
|
||||
{
|
||||
var currentFighter = HappeningData.fighterTurn.Current;
|
||||
var targetFighter = FightWorld.Instance.allyFighters.vesnaFighter;
|
||||
|
||||
var currentFighterVisual = allFightersVisual.GetVisualForFighter(currentFighter);
|
||||
var targetFighterVisual = allFightersVisual.GetVisualForFighter(targetFighter);
|
||||
|
||||
await currentFighterVisual.AnimatePosToTarget(targetFighterVisual);
|
||||
_ = targetFighterVisual.AnimateHit();
|
||||
await currentFighterVisual.AnimatePosToBase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dlik4vktiu7dg
|
||||
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Babushka.scripts.CSharp.Common.Fight.ActionDetails;
|
||||
using Babushka.scripts.CSharp.Common.Util;
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public partial class AllFightersVisual : Node
|
||||
{
|
||||
[ExportCategory("References")] [Export]
|
||||
private Node2D _allyFighters = null!;
|
||||
|
||||
[Export] private Node2D _enemyFighters = null!;
|
||||
|
||||
// TODO: move type to prefab mapping to Resource
|
||||
[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)
|
||||
{
|
||||
if (to == FightHappening.FightState.FightersEnterAnim)
|
||||
{
|
||||
EnterFighter();
|
||||
}
|
||||
|
||||
if (to == FightHappening.FightState.InputActionDetail)
|
||||
{
|
||||
if (HappeningData.actionStaging!.CurrentDetail() is TargetSelectActionDetail targetDetail)
|
||||
{
|
||||
ShowTargetSelect(targetDetail);
|
||||
}
|
||||
}
|
||||
|
||||
if (from == FightHappening.FightState.InputActionDetail)
|
||||
{
|
||||
HideTargetSelect();
|
||||
}
|
||||
|
||||
if (from == FightHappening.FightState.ActionAnim)
|
||||
{
|
||||
_fighterVisuals.Values.ForEach(fv => fv.UpdateHealthBar());
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// TODO: use Event bus
|
||||
if (targetDetail.selectEnemy)
|
||||
_fighterVisuals
|
||||
.Where(kv => kv.Key.IsInFormation(HappeningData.enemyFighterFormation))
|
||||
.ForEach(kv => kv.Value.SetTargetSelectionActive(true));
|
||||
|
||||
if (targetDetail.selectAlly)
|
||||
_fighterVisuals
|
||||
.Where(kv => kv.Key.IsInFormation(HappeningData.allyFighterFormation))
|
||||
.ForEach(kv => kv.Value.SetTargetSelectionActive(true));
|
||||
}
|
||||
|
||||
private void HideTargetSelect()
|
||||
{
|
||||
foreach (var visual in _fighterVisuals.Values)
|
||||
{
|
||||
visual.SetTargetSelectionActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public FighterVisual GetVisualForFighter(FightWorld.Fighter fighter)
|
||||
{
|
||||
return _fighterVisuals.TryGetValue(fighter, out var visual)
|
||||
? visual
|
||||
: throw new InvalidOperationException("No visual for this fighter");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dwsqst8fhhqlc
|
||||
@@ -0,0 +1,30 @@
|
||||
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,
|
||||
availableActions =
|
||||
[
|
||||
new AllyAttackAction()
|
||||
]
|
||||
};
|
||||
public FightWorld.Fighter chuhaFighter = new()
|
||||
{
|
||||
type = FightWorld.Fighter.Type.Chuha,
|
||||
maxHealth = 15,
|
||||
availableActions =
|
||||
[
|
||||
new FighterAction.Skip()
|
||||
]
|
||||
};
|
||||
|
||||
public bool IsAlive()
|
||||
{
|
||||
return vesnaFighter.IsAlive();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dst8xcyiw18uc
|
||||
@@ -0,0 +1,386 @@
|
||||
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 partial class FightHappening : Node
|
||||
{
|
||||
/*
|
||||
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,
|
||||
}
|
||||
|
||||
public class FightersEnterStaging
|
||||
{
|
||||
public required List<FightWorld.Fighter> enteringAllyFighters;
|
||||
public required List<FightWorld.Fighter> enteringEnemyFighters;
|
||||
|
||||
public bool HasAnyToExecute()
|
||||
{
|
||||
return enteringAllyFighters.Any() || enteringEnemyFighters.Any();
|
||||
}
|
||||
}
|
||||
|
||||
#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.fighterTurn.Current;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
[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 Singleton
|
||||
|
||||
public static FightHappening Instance = null!;
|
||||
|
||||
private void SetupInstance()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
SetupInstance();
|
||||
StartFight();
|
||||
}
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public void StartFight()
|
||||
{
|
||||
RequireState(FightState.None);
|
||||
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;
|
||||
TransitionToState(nextState);
|
||||
|
||||
EmitSignalSignalTransitionFromState(lastState);
|
||||
EmitSignalSignalTransitionState(lastState, nextState);
|
||||
EmitSignalSignalTransitionToState(nextState);
|
||||
_inTransition = false;
|
||||
|
||||
if (_changeToAfterTransition.HasValue)
|
||||
{
|
||||
ChangeState(_changeToAfterTransition.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void TransitionFromState()
|
||||
{
|
||||
// fixed behaviour
|
||||
switch (HappeningData.fightState)
|
||||
{
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
private void TransitionToState(FightState nextState)
|
||||
{
|
||||
// fixed behaviour
|
||||
switch (HappeningData.fightState)
|
||||
{
|
||||
case FightState.FightStartAnim:
|
||||
AdvanceToStateInSeconds(FightState.FightersEnter, StartAnimationTime);
|
||||
break;
|
||||
case FightState.FightersEnter:
|
||||
HappeningData.fightersEnterStaging = StageFightersEnter();
|
||||
if (HappeningData.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 and fighter enter staging
|
||||
HappeningData.actionStaging = null;
|
||||
HappeningData.fightersEnterStaging = null;
|
||||
|
||||
if (!FightWorld.Instance.allyFighters.IsAlive())
|
||||
{
|
||||
ChangeState(FightState.EnemyWin);
|
||||
}
|
||||
else if (HappeningData.enemyGroup.AreAllDead())
|
||||
{
|
||||
ChangeState(FightState.PlayerWin);
|
||||
}
|
||||
else if (CurrentFighter.actionPointsLeft <= 0)
|
||||
{
|
||||
ChangeState(FightState.FightersEnter);
|
||||
}
|
||||
else if (CurrentFighter.IsInFormation(HappeningData.enemyFighterFormation))
|
||||
{
|
||||
ChangeState(FightState.EnemyActionSelect);
|
||||
}
|
||||
else
|
||||
{
|
||||
ChangeState(FightState.InputActionSelect);
|
||||
}
|
||||
|
||||
break;
|
||||
case FightState.InputActionSelect:
|
||||
// wait for player input
|
||||
break;
|
||||
case FightState.ActionCheckDetails:
|
||||
RequireNotNull(HappeningData.actionStaging);
|
||||
|
||||
if (ActionAbort())
|
||||
ChangeState(FightState.InputActionSelect);
|
||||
else if (ActionNeededDetail())
|
||||
ChangeState(FightState.InputActionDetail);
|
||||
else
|
||||
ChangeState(FightState.ActionExecute);
|
||||
break;
|
||||
case FightState.InputActionDetail:
|
||||
// wait for player input
|
||||
break;
|
||||
case FightState.EnemyActionSelect:
|
||||
HappeningData.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;
|
||||
case FightState.EnemyWin:
|
||||
// TODO: remove and find proper solution
|
||||
ReviveVesna();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Game Logic
|
||||
|
||||
private FightersEnterStaging StageFightersEnter()
|
||||
{
|
||||
// ally
|
||||
var enteringAllyFighters = new List<FightWorld.Fighter>();
|
||||
var allyFighters = FightWorld.Instance.allyFighters;
|
||||
if (!allyFighters.vesnaFighter.IsInFormation(HappeningData.allyFighterFormation))
|
||||
{
|
||||
enteringAllyFighters.Add(allyFighters.vesnaFighter);
|
||||
}
|
||||
|
||||
// enemy
|
||||
const int totalEnemySpace = 3;
|
||||
var enemySpaceLeft = HappeningData.enemyFighterFormation.GetEmptySlotCount();
|
||||
|
||||
return new FightersEnterStaging
|
||||
{
|
||||
enteringAllyFighters = enteringAllyFighters,
|
||||
enteringEnemyFighters = HappeningData.enemyGroup.fighters
|
||||
.WhereIsAlive()
|
||||
.WhereIsNotInFormation(HappeningData.enemyFighterFormation)
|
||||
.Take(enemySpaceLeft)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private void ExecuteFightersEnter()
|
||||
{
|
||||
Debug.Assert(HappeningData.fightersEnterStaging != null);
|
||||
foreach (var fighter in HappeningData.fightersEnterStaging.enteringAllyFighters)
|
||||
{
|
||||
var emptySlotIndex = HappeningData.allyFighterFormation.GetFirstEmptySlot();
|
||||
if (emptySlotIndex < 0) throw new Exception("No empty slot for ally fighter to enter");
|
||||
HappeningData.allyFighterFormation.SetFighterAtPosition(emptySlotIndex, fighter);
|
||||
HappeningData.fighterTurn.AddAsLast(fighter);
|
||||
}
|
||||
|
||||
foreach (var fighter in HappeningData.fightersEnterStaging.enteringEnemyFighters)
|
||||
{
|
||||
var emptySlotIndex = HappeningData.enemyFighterFormation.GetFirstEmptySlot();
|
||||
if (emptySlotIndex < 0) throw new Exception("No empty slot for enemy fighter to enter");
|
||||
HappeningData.enemyFighterFormation.SetFighterAtPosition(emptySlotIndex, fighter);
|
||||
HappeningData.fighterTurn.AddAsLast(fighter);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteNextFighter()
|
||||
{
|
||||
HappeningData.fighterTurn.Next();
|
||||
CurrentFighter.actionPointsLeft = FightWorld.Fighter.MaxActionPoints;
|
||||
}
|
||||
|
||||
private void ExecuteAction()
|
||||
{
|
||||
Debug.Assert(HappeningData.actionStaging != null);
|
||||
HappeningData.actionStaging.ExecuteAction();
|
||||
CurrentFighter.actionPointsLeft -= HappeningData.actionStaging.GetActionPointCost();
|
||||
}
|
||||
|
||||
private Variant<float, Func<bool>> GetActionAnimationEnd()
|
||||
{
|
||||
Debug.Assert(HappeningData.actionStaging != null);
|
||||
return HappeningData.actionStaging.GetAnimationEnd();
|
||||
}
|
||||
|
||||
private bool ActionAbort()
|
||||
{
|
||||
Debug.Assert(HappeningData.actionStaging != null);
|
||||
return HappeningData.actionStaging.MarkedForAbort();
|
||||
}
|
||||
|
||||
private bool ActionNeededDetail()
|
||||
{
|
||||
Debug.Assert(HappeningData.actionStaging != null);
|
||||
return HappeningData.actionStaging.NextDetail();
|
||||
}
|
||||
|
||||
// TODO: remove
|
||||
private void ReviveVesna()
|
||||
{
|
||||
var vesnaFighter = FightWorld.Instance.allyFighters.vesnaFighter;
|
||||
vesnaFighter.health = vesnaFighter.maxHealth;
|
||||
GD.Print("Vesna has been revived. This is for the current prototype only");
|
||||
}
|
||||
|
||||
#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 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)
|
||||
{
|
||||
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
|
||||
@@ -0,0 +1,57 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public partial class FightHappeningStateDebugger : Node
|
||||
{
|
||||
[Export] private Label _label;
|
||||
|
||||
[Export] private Label _current;
|
||||
|
||||
private FightWorld.FightHappeningData Data => FightWorld.Instance.fightHappeningData!;
|
||||
|
||||
public void StateChange(FightHappening.FightState from, FightHappening.FightState to)
|
||||
{
|
||||
_current.Text = $"{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,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Babushka.scripts.CSharp.Common.Fight;
|
||||
using Babushka.scripts.CSharp.Common.Fight.ActionDetails;
|
||||
using Babushka.scripts.CSharp.Common.Minigame;
|
||||
|
||||
public partial class FightMinigameHandler : Node
|
||||
{
|
||||
#region Shortcuts
|
||||
|
||||
private FightWorld.FightHappeningData HappeningData => FightWorld.Instance.fightHappeningData ?? throw new NoFightHappeningException();
|
||||
|
||||
#endregion
|
||||
|
||||
[Export] private MinigameController _minigameController;
|
||||
|
||||
|
||||
public void OnStateEnter(FightHappening.FightState to)
|
||||
{
|
||||
if(to!=FightHappening.FightState.InputActionDetail) return;
|
||||
|
||||
var currentDetail = HappeningData.actionStaging!.CurrentDetail();
|
||||
if(currentDetail is not MinigameActionDetail minigameDetail) return;
|
||||
|
||||
_minigameController.Run(new MinigameController.Builder<int>()
|
||||
.AddRegion(4).RegionWithText("4").RegionWithTheme(MinigameController.RegionTheme.Normal)
|
||||
.AddRegion(0).RegionWithProportion(1.5f).RegionWithText("0").RegionWithTheme(MinigameController.RegionTheme.Bad)
|
||||
.AddRegion(8).RegionWithProportion(0.5f).RegionWithText("8").RegionWithTheme(MinigameController.RegionTheme.VeryGood)
|
||||
.AddRegion(0).RegionWithProportion(1.5f).RegionWithText("0").RegionWithTheme(MinigameController.RegionTheme.Bad)
|
||||
.AddRegion(3).RegionWithText("3").RegionWithTheme(MinigameController.RegionTheme.NormalAlt1)
|
||||
.AddRegion(5).RegionWithText("5").RegionWithTheme(MinigameController.RegionTheme.NormalAlt2)
|
||||
.WithHitCount(3)
|
||||
).ContinueWith(task =>
|
||||
{
|
||||
minigameDetail.damageHits = task.Result;
|
||||
//FightHappening.Instance.DetailFilled();
|
||||
// Apparently ContinueWith spawn a new Thread, but methods on a node only want to be called from the main thread
|
||||
FightHappening.Instance.CallDeferred("DetailFilled");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bwm0nhvt1083k
|
||||
@@ -0,0 +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(PropertyHint.ArrayType)] private Node2D[] _enemyGroupSpawns;
|
||||
[Export] private PackedScene _roamingEnemyGroupPrefab;
|
||||
[Export] private FightSceneSwitcher _fightSceneSwitcher;
|
||||
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
var room = FightWorld.Instance.currentRoom!;
|
||||
|
||||
var i = 0;
|
||||
foreach (var availableParent in _enemyGroupSpawns.Shuffle())
|
||||
{
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbu8afaiohpdh
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Babushka.scripts.CSharp.Common.SceneManagement;
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public partial class FightSceneSwitcher : Node
|
||||
{
|
||||
[Export] private Node _sceneRoot = null!;
|
||||
[Export] private string _fightRoomScenePath = null!;
|
||||
[Export] private string _fightHappeningScene = null!;
|
||||
|
||||
private void LoadNext()
|
||||
{
|
||||
var nextRoom = FightWorld.Instance.currentRoom;
|
||||
Debug.Assert(nextRoom != null, "nextRoom!=null");
|
||||
var nextFightHappening = FightWorld.Instance.fightHappeningData;
|
||||
SceneTransitionThreaded.Instance.ChangeSceneToFile(nextFightHappening != null
|
||||
? _fightHappeningScene
|
||||
: _fightRoomScenePath);
|
||||
_ = UnloadAfterDelay();
|
||||
}
|
||||
|
||||
private async Task UnloadAfterDelay()
|
||||
{
|
||||
await ToSignal(GetTree().CreateTimer(1.0f), "timeout"); // 1.0f seconds
|
||||
//sceneRoot.QueueFree();
|
||||
}
|
||||
|
||||
public void SwitchRoom(int pathIndex)
|
||||
{
|
||||
Debug.Assert(FightWorld.Instance.currentRoom != null, "FightWorld.Instance.currentRoom!=null");
|
||||
|
||||
if (!FightWorld.Instance.currentRoom.paths.TryGetValue(pathIndex, out var nextRoom))
|
||||
throw new Exception("Trying to go down a non-existent path");
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public void ExitFight()
|
||||
{
|
||||
if (FightWorld.Instance.fightHappeningData == null)
|
||||
throw new Exception("Trying to exit a fight while not in a fight");
|
||||
|
||||
FightWorld.Instance.fightHappeningData = null;
|
||||
LoadNext();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cql8mt5jsmcdl
|
||||
@@ -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
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public static class FightUtils
|
||||
{
|
||||
public static IEnumerable<FightWorld.Fighter> WhereIsAlive(this IEnumerable<FightWorld.Fighter> self)
|
||||
{
|
||||
return self.Where(e => e.IsAlive());
|
||||
}
|
||||
|
||||
public static IEnumerable<FightWorld.Fighter> WhereIsNotInFormation(this IEnumerable<FightWorld.Fighter> self, FighterFormation formation)
|
||||
{
|
||||
return self.Where(e => !e.IsInFormation(formation));
|
||||
}
|
||||
|
||||
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 Math.Max(self.health ?? self.maxHealth, 0);
|
||||
}
|
||||
|
||||
public static void AddHealth(this FightWorld.Fighter self, int addHealth)
|
||||
{
|
||||
self.health = self.GetHealth() + addHealth;
|
||||
}
|
||||
|
||||
public static bool IsInFormation(this FightWorld.Fighter self, FighterFormation formation)
|
||||
{
|
||||
return formation.ContainsFighter(self);
|
||||
}
|
||||
|
||||
public static bool AreAllDead(this FightWorld.FighterGroup self)
|
||||
{
|
||||
return self.fighters.All(e => e.IsDead());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://beuhpltb84pf
|
||||
@@ -0,0 +1,191 @@
|
||||
using System.Collections.Generic;
|
||||
using Babushka.scripts.CSharp.Common.Fight.Actions;
|
||||
using Babushka.scripts.CSharp.Common.Util;
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public partial class FightWorld : Node
|
||||
{
|
||||
public class World
|
||||
{
|
||||
public required List<Room> rooms;
|
||||
}
|
||||
|
||||
public class Room
|
||||
{
|
||||
public required Dictionary<int, Room> paths;
|
||||
public required List<FighterGroup> enemyGroups;
|
||||
}
|
||||
|
||||
public class FighterGroup
|
||||
{
|
||||
public required List<Fighter> fighters;
|
||||
}
|
||||
|
||||
public class FightHappeningData
|
||||
{
|
||||
public required FighterGroup enemyGroup;
|
||||
public FightHappening.FightState fightState = FightHappening.FightState.None;
|
||||
public readonly FighterTurn fighterTurn = new();
|
||||
public readonly FighterFormation allyFighterFormation = new();
|
||||
public readonly FighterFormation enemyFighterFormation = new();
|
||||
public FightHappening.FightersEnterStaging? fightersEnterStaging;
|
||||
public FighterAction? actionStaging;
|
||||
}
|
||||
|
||||
public class Fighter
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
Blob,
|
||||
BigBlob,
|
||||
Mavka,
|
||||
YourMom,
|
||||
Vesna,
|
||||
Chuha
|
||||
}
|
||||
|
||||
public required Type type;
|
||||
public required int maxHealth;
|
||||
public required List<FighterAction> availableActions;
|
||||
public const int MaxActionPoints = 1;
|
||||
public int? health = null; // null => initialize to full health on spawn
|
||||
public int actionPointsLeft;
|
||||
|
||||
public FighterAction AutoSelectAction()
|
||||
{
|
||||
return availableActions.Random() ?? new FighterAction.Skip();
|
||||
}
|
||||
}
|
||||
|
||||
#region AutoLoad ( Contains _EnterTree() )
|
||||
|
||||
public static FightWorld Instance { get; private set; } = null!;
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
Instance = this;
|
||||
MyEnterTree();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public World? world = null;
|
||||
public Room? currentRoom = null;
|
||||
public FightHappeningData? fightHappeningData = null;
|
||||
public AllyFighters allyFighters = new();
|
||||
|
||||
public void MyEnterTree()
|
||||
{
|
||||
Generate();
|
||||
currentRoom = world!.rooms[0];
|
||||
}
|
||||
|
||||
public void Generate()
|
||||
{
|
||||
world = new Generator().GenerateWorld();
|
||||
}
|
||||
|
||||
private class Generator
|
||||
{
|
||||
public World GenerateWorld()
|
||||
{
|
||||
var world = new World
|
||||
{
|
||||
rooms = GenerateRooms()
|
||||
};
|
||||
return world;
|
||||
}
|
||||
|
||||
private List<Room> GenerateRooms()
|
||||
{
|
||||
var rooms = new List<Room>();
|
||||
|
||||
var roomCount = 2;
|
||||
|
||||
for (var i = 0; i < roomCount; i++)
|
||||
{
|
||||
rooms.Add(GenerateDisconnectedRoom());
|
||||
}
|
||||
|
||||
// Connect rooms linearly
|
||||
for (var i = 0; i < rooms.Count - 1; i++)
|
||||
{
|
||||
rooms[i].paths[0] = rooms[i + 1];
|
||||
rooms[i + 1].paths[1] = rooms[i];
|
||||
}
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
private Room GenerateDisconnectedRoom()
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
paths = new Dictionary<int, Room>(),
|
||||
enemyGroups = GenerateEnemyGroups()
|
||||
};
|
||||
return room;
|
||||
}
|
||||
|
||||
private List<FighterGroup> GenerateEnemyGroups()
|
||||
{
|
||||
var enemyGroups = new List<FighterGroup>();
|
||||
|
||||
var enemyGroupCount = GD.RandRange(1, 3);
|
||||
|
||||
for (var i = 0; i < enemyGroupCount; i++)
|
||||
{
|
||||
enemyGroups.Add(GenerateSingleEnemyGroup());
|
||||
}
|
||||
|
||||
return enemyGroups;
|
||||
}
|
||||
|
||||
private FighterGroup GenerateSingleEnemyGroup()
|
||||
{
|
||||
var enemyGroup = new FighterGroup
|
||||
{
|
||||
fighters = []
|
||||
};
|
||||
|
||||
var enemyCount = GD.RandRange(1, 3);
|
||||
|
||||
for (var i = 0; i < enemyCount; i++)
|
||||
{
|
||||
enemyGroup.fighters.Add(GenerateSingleEnemy());
|
||||
}
|
||||
|
||||
return enemyGroup;
|
||||
}
|
||||
|
||||
private Fighter GenerateSingleEnemy()
|
||||
{
|
||||
var typeRoll = GD.RandRange(0, 99);
|
||||
|
||||
// Disabled generating different types due to lack of fighter visual type implementation
|
||||
//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
|
||||
{
|
||||
type = type,
|
||||
health = null,
|
||||
maxHealth = 12,
|
||||
availableActions =
|
||||
[
|
||||
new BlobAttackAction()
|
||||
]
|
||||
};
|
||||
|
||||
return enemy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dqe1i2qmpttwf
|
||||
@@ -1,181 +0,0 @@
|
||||
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;
|
||||
[Export] public int maxActions = 1;
|
||||
|
||||
[ExportCategory("References")]
|
||||
[Export] private Node2D _attackButtons;
|
||||
[Export] private Node2D _targetButtons;
|
||||
[Export] private Node2D _targetMarker;
|
||||
[Export] private Label _healthText;
|
||||
[Export] private Node2D _visualSprite;
|
||||
|
||||
[Signal] public delegate void DamageTakenEventHandler();
|
||||
[Signal] public delegate void AttackingEventHandler();
|
||||
[Signal] public delegate void DyingEventHandler();
|
||||
[Signal] public delegate void HealedEventHandler();
|
||||
|
||||
|
||||
private int _health;
|
||||
private int _actions;
|
||||
|
||||
|
||||
public FightInstance fightInstance;
|
||||
public int Health
|
||||
{
|
||||
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;
|
||||
UpdateHealthVisual();
|
||||
ResetActions();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public void HealMouseEvent(Node viewport, InputEvent inputEvent, int shapeIdx)
|
||||
{
|
||||
if (inputEvent.IsPressed())
|
||||
ClickedHeal();
|
||||
}
|
||||
|
||||
private void ClickedAttack()
|
||||
{
|
||||
fightInstance.SelectAttack(this);
|
||||
}
|
||||
|
||||
private void ClickedHeal()
|
||||
{
|
||||
fightInstance.SelectHeal(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)
|
||||
{
|
||||
EmitSignalAttacking();
|
||||
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)
|
||||
{
|
||||
EmitSignalDamageTaken();
|
||||
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;
|
||||
}
|
||||
|
||||
public void ResetActions()
|
||||
{
|
||||
_actions = maxActions;
|
||||
}
|
||||
|
||||
public bool HasActionsLeft()
|
||||
{
|
||||
return _actions > 0;
|
||||
}
|
||||
|
||||
public void DecrementActions()
|
||||
{
|
||||
_actions--;
|
||||
}
|
||||
|
||||
public void HealAnimation()
|
||||
{
|
||||
EmitSignalHealed();
|
||||
var tween = GetTree().CreateTween();
|
||||
tween.TweenProperty(this, "scale", new Vector2(0.6f, 1.4f), 0.15);
|
||||
tween.TweenProperty(this, "scale", new Vector2(1, 1), 0.4)
|
||||
.SetTrans(Tween.TransitionType.Cubic).SetEase(Tween.EaseType.Out);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Babushka.scripts.CSharp.Common.Util;
|
||||
using Godot;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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<bool></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<bool></c>, the animation will be done, when the function returns true.
|
||||
/// </returns>
|
||||
public abstract Variant<float, Func<bool>> GetAnimationEnd();
|
||||
|
||||
/// <summary>
|
||||
/// Animates the action.
|
||||
/// </summary>
|
||||
/// <param name="allFightersVisual"></param>
|
||||
public virtual async Task AnimateAction(AllFightersVisual allFightersVisual)
|
||||
{
|
||||
}
|
||||
|
||||
public void MarkAbort()
|
||||
{
|
||||
_abort = true;
|
||||
}
|
||||
|
||||
public bool MarkedForAbort()
|
||||
{
|
||||
return _abort;
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
public override Variant<float, Func<bool>> GetAnimationEnd()
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
public override bool NextDetail()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c60jugfee0bpv
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public class FighterFormation
|
||||
{
|
||||
private readonly List<FightWorld.Fighter?> _fighters;
|
||||
|
||||
public FighterFormation(int slots = 3)
|
||||
{
|
||||
_fighters = [];
|
||||
for (var i = 0; i < slots; i++)
|
||||
{
|
||||
_fighters.Add(null);
|
||||
}
|
||||
}
|
||||
|
||||
public FightWorld.Fighter? GetFighterAtPosition(int position)
|
||||
{
|
||||
Debug.Assert(position >= 0, "position>=0");
|
||||
Debug.Assert(position < _fighters.Count, "position does not exist");
|
||||
|
||||
return _fighters[position];
|
||||
}
|
||||
|
||||
public void SetFighterAtPosition(int position, FightWorld.Fighter value)
|
||||
{
|
||||
Debug.Assert(position >= 0, "position>=0");
|
||||
Debug.Assert(position < _fighters.Count, "position does not exist");
|
||||
|
||||
_fighters[position] = value;
|
||||
}
|
||||
|
||||
public bool ContainsFighter(FightWorld.Fighter fighter)
|
||||
{
|
||||
return _fighters.Contains(fighter);
|
||||
}
|
||||
|
||||
public int GetFirstEmptySlot()
|
||||
{
|
||||
for (var i = 0; i < _fighters.Count; i++)
|
||||
{
|
||||
if (_fighters[i] == null)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int GetEmptySlotCount()
|
||||
{
|
||||
return _fighters.Count(fighter => fighter == null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://oipfvb87uyq3
|
||||
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public class FighterTurn
|
||||
{
|
||||
private class Node
|
||||
{
|
||||
public required Node next;
|
||||
public required FightWorld.Fighter fighter;
|
||||
}
|
||||
|
||||
private Node? _currentNode;
|
||||
|
||||
public FightWorld.Fighter Current => _currentNode?.fighter ?? throw new InvalidOperationException("No current fighter");
|
||||
|
||||
public void Next()
|
||||
{
|
||||
Debug.Assert(_currentNode != null, "currentNode!=null");
|
||||
_currentNode = _currentNode.next;
|
||||
}
|
||||
|
||||
public FightWorld.Fighter PeekNext()
|
||||
{
|
||||
Debug.Assert(_currentNode != null, "currentNode!=null");
|
||||
return _currentNode.next.fighter;
|
||||
}
|
||||
|
||||
public void AddAsLast(FightWorld.Fighter value)
|
||||
{
|
||||
// if first node
|
||||
if (_currentNode == null)
|
||||
{
|
||||
_currentNode = new Node { fighter = value, next = null! };
|
||||
_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 by loss of reference on the next Next() call
|
||||
|
||||
node.next = node.next.next;
|
||||
return true;
|
||||
}
|
||||
|
||||
node = node.next;
|
||||
} while (node != _currentNode);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bahm4ukspymm2
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Babushka.scripts.CSharp.Common.Fight.ActionDetails;
|
||||
using Babushka.scripts.CSharp.Common.Fight.UI;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
|
||||
public partial class FighterVisual : Node2D
|
||||
{
|
||||
#region Shortcuts
|
||||
|
||||
private FightWorld.FightHappeningData HappeningData =>
|
||||
FightWorld.Instance.fightHappeningData ?? throw new NoFightHappeningException();
|
||||
|
||||
#endregion
|
||||
|
||||
[ExportCategory("References")]
|
||||
[Export] private Node2D _visualParent = null!;
|
||||
[Export] private Node2D _targetSelectionParent = null!;
|
||||
[Export] public FighterHealthBarVisual healthBarVisual = null!;
|
||||
|
||||
|
||||
private FightWorld.Fighter _boundFighter;
|
||||
|
||||
public void Initialize(FightWorld.Fighter fighter)
|
||||
{
|
||||
_boundFighter = fighter;
|
||||
UpdateMirrorState();
|
||||
UpdateHealthBar();
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
_visualParent.Scale = new Vector2(_boundFighter.IsInFormation(HappeningData.enemyFighterFormation) ? -1 : 1, 1);
|
||||
}
|
||||
|
||||
public void UpdateHealthBar()
|
||||
{
|
||||
healthBarVisual.UpdateHealth(_boundFighter.GetHealth(), _boundFighter.maxHealth);
|
||||
}
|
||||
|
||||
public void SetTargetSelectionActive(bool value)
|
||||
{
|
||||
_targetSelectionParent.Visible = value;
|
||||
_targetSelectionParent.ProcessMode = value ? ProcessModeEnum.Inherit : ProcessModeEnum.Disabled;
|
||||
}
|
||||
|
||||
// listen from inside
|
||||
public void ClickedTarget()
|
||||
{
|
||||
if (HappeningData.actionStaging!.CurrentDetail() is not TargetSelectActionDetail targetDetail)
|
||||
throw new InvalidOperationException("No target selection needed right now");
|
||||
|
||||
targetDetail.SetTarget(_boundFighter);
|
||||
FightHappening.Instance.DetailFilled();
|
||||
}
|
||||
|
||||
// Animations
|
||||
public async Task AnimatePosToTarget(FighterVisual targetVisual, double duration = 0.15)
|
||||
{
|
||||
var tween = GetTree().CreateTween();
|
||||
tween.TweenProperty(_visualParent, "global_position", targetVisual.GlobalPosition, 0.15);
|
||||
await ToSignal(tween, "finished");
|
||||
}
|
||||
|
||||
public async Task AnimatePosToBase(double duration = 0.7)
|
||||
{
|
||||
var tween = GetTree().CreateTween();
|
||||
tween.TweenProperty(_visualParent, "position", new Vector2(0, 0), 0.15);
|
||||
await ToSignal(tween, "finished");
|
||||
}
|
||||
|
||||
public async Task AnimateHit()
|
||||
{
|
||||
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);
|
||||
await ToSignal(tween, "finished");
|
||||
}
|
||||
|
||||
// Keep for reference for new Heal animation
|
||||
//public void HealAnimation()
|
||||
//{
|
||||
// EmitSignalHealed();
|
||||
// var tween = GetTree().CreateTween();
|
||||
// tween.TweenProperty(this, "scale", new Vector2(0.6f, 1.4f), 0.15);
|
||||
// tween.TweenProperty(this, "scale", new Vector2(1, 1), 0.4)
|
||||
// .SetTrans(Tween.TransitionType.Cubic).SetEase(Tween.EaseType.Out);
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public partial class NextRoomTrigger : Area2D
|
||||
{
|
||||
[Export] private int pathIndex;
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
BodyEntered += _OnBodyEnter;
|
||||
}
|
||||
|
||||
private void _OnBodyEnter(Node2D other)
|
||||
{
|
||||
var fss = GetNode<FightSceneSwitcher>("%FightSceneSwitcher");
|
||||
fss.SwitchRoom(pathIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bryibv73x5iwr
|
||||
@@ -0,0 +1,7 @@
|
||||
using System;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public class NoFightHappeningException() : Exception("No fight happening right now")
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b2n37glcxm8wv
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Diagnostics;
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public partial class PathSetup : Node
|
||||
{
|
||||
[Export] private int pathId;
|
||||
|
||||
[ExportCategory("Variants")] [Export] private CanvasItem closedVariant;
|
||||
[Export] private CanvasItem nextRoomVariant;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
SetupPathVariant();
|
||||
}
|
||||
|
||||
private void SetupPathVariant()
|
||||
{
|
||||
Debug.Assert(FightWorld.Instance.currentRoom != null);
|
||||
if (FightWorld.Instance.currentRoom.paths.TryGetValue(pathId, out var nextRoom))
|
||||
{
|
||||
ShowOnlyVariant(nextRoomVariant);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowOnlyVariant(closedVariant);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowOnlyVariant(CanvasItem variantToShow)
|
||||
{
|
||||
HideVariant(closedVariant);
|
||||
HideVariant(nextRoomVariant);
|
||||
ShowVariant(variantToShow);
|
||||
}
|
||||
|
||||
private void ShowVariant(CanvasItem variant)
|
||||
{
|
||||
variant.Visible = true;
|
||||
variant.ProcessMode = ProcessModeEnum.Always;
|
||||
}
|
||||
|
||||
private void HideVariant(CanvasItem variant)
|
||||
{
|
||||
variant.Visible = false;
|
||||
variant.ProcessMode = ProcessModeEnum.Disabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dpkx2gbg7b5xh
|
||||
@@ -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,22 @@
|
||||
using System.Threading.Tasks;
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight;
|
||||
|
||||
public partial class SwitchSceneOnFightEnd : Node
|
||||
{
|
||||
[Export] private FightSceneSwitcher _fightSceneSwitcher = null!;
|
||||
|
||||
public void OnFightStateEnter(FightHappening.FightState to)
|
||||
{
|
||||
if (to is FightHappening.FightState.PlayerWin
|
||||
or FightHappening.FightState.EnemyWin)
|
||||
_ = SwitchSceneAfterTime(2.0f);
|
||||
}
|
||||
|
||||
private async Task SwitchSceneAfterTime(float seconds)
|
||||
{
|
||||
await ToSignal(GetTree().CreateTimer(seconds), "timeout");
|
||||
_fightSceneSwitcher.ExitFight();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://2f7rqk50gtdg
|
||||
@@ -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.fighterTurn.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,14 @@
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Fight.UI;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class FighterHealthBarVisual : Node2D
|
||||
{
|
||||
[Export] private Label _tmpHealthLabel = null!;
|
||||
|
||||
public void UpdateHealth(int currentHealth, int maxHealth)
|
||||
{
|
||||
_tmpHealthLabel.Text = $"{currentHealth} / {maxHealth}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b2dx06p6i7pu0
|
||||
@@ -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
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Babushka.scripts.CSharp.Common.Util;
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Minigame;
|
||||
|
||||
public partial class MinigameController : Node2D
|
||||
{
|
||||
public enum RegionTheme
|
||||
{
|
||||
Disabled,
|
||||
VeryBad,
|
||||
Bad,
|
||||
Normal,
|
||||
NormalAlt1,
|
||||
NormalAlt2,
|
||||
God,
|
||||
VeryGood
|
||||
}
|
||||
|
||||
public class Builder<T>
|
||||
{
|
||||
internal class Region
|
||||
{
|
||||
public required T value;
|
||||
public float proportion = 1f;
|
||||
public string text = "";
|
||||
public RegionTheme theme = RegionTheme.Normal;
|
||||
}
|
||||
|
||||
public enum DoubleHitHandling
|
||||
{
|
||||
Allow,
|
||||
Disallow,
|
||||
Remove,
|
||||
}
|
||||
|
||||
internal List<Region> regions = new();
|
||||
internal DoubleHitHandling doubleHitHandling = DoubleHitHandling.Allow;
|
||||
internal int maxHitCount = 3;
|
||||
|
||||
// add region
|
||||
public Builder<T> AddRegion(T value)
|
||||
{
|
||||
regions.Add(new Region { value = value });
|
||||
return this;
|
||||
}
|
||||
|
||||
// region settings
|
||||
public Builder<T> RegionWithProportion(float proportion)
|
||||
{
|
||||
if (regions.Count == 0)
|
||||
throw new InvalidOperationException("No region to set proportion for");
|
||||
|
||||
regions.Last().proportion = proportion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> RegionWithText(string text)
|
||||
{
|
||||
if (regions.Count == 0)
|
||||
throw new InvalidOperationException("No region to set text for");
|
||||
|
||||
regions.Last().text = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> RegionWithTheme(RegionTheme theme)
|
||||
{
|
||||
if (regions.Count == 0)
|
||||
throw new InvalidOperationException("No region to set theme for");
|
||||
|
||||
regions.Last().theme = theme;
|
||||
return this;
|
||||
}
|
||||
|
||||
// general settings
|
||||
public Builder<T> WithDoubleHitHandling(DoubleHitHandling handling)
|
||||
{
|
||||
if (handling != DoubleHitHandling.Allow)
|
||||
throw new NotImplementedException();
|
||||
|
||||
doubleHitHandling = handling;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> WithHitCount(int hitCount)
|
||||
{
|
||||
this.maxHitCount = hitCount;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private TaskCompletionSource? _minigameComplete;
|
||||
private List<int>? _hits;
|
||||
private float _armPosition = 0f;
|
||||
private float _armSpeed = 1f;
|
||||
private List<float>? _regions;
|
||||
private int _maxHitCount;
|
||||
|
||||
[Export] private PackedScene _regionVisualPrefab = null!;
|
||||
[Export] private Node2D _regionsParent = null!;
|
||||
[Export] private Color _baseRegionColor = Colors.Red;
|
||||
|
||||
[Signal] public delegate void ArmMovedEventHandler(float newPos);
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
HideMinigame();
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
_armPosition += _armSpeed * (float)delta;
|
||||
_armPosition = Mathf.PosMod(_armPosition, 1);
|
||||
EmitSignalArmMoved(_armPosition);
|
||||
}
|
||||
|
||||
public async Task<List<T>> Run<T>(Builder<T> builder)
|
||||
{
|
||||
ShowMinigame();
|
||||
Setup(builder);
|
||||
await _minigameComplete!.Task;
|
||||
var returnValue = _hits!.Select(h => builder.regions[h].value).ToList();
|
||||
Reset();
|
||||
HideMinigame();
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public void Hit()
|
||||
{
|
||||
if (_hits == null) return;
|
||||
|
||||
int i;
|
||||
for (i = 0; i < _regions!.Count - 1; i++)
|
||||
{
|
||||
if (_armPosition < _regions[i])
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_hits.Add(i);
|
||||
|
||||
_armSpeed = -_armSpeed;
|
||||
|
||||
if (_hits.Count >= _maxHitCount)
|
||||
{
|
||||
_minigameComplete!.SetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private void Setup<T>(Builder<T> builder)
|
||||
{
|
||||
_minigameComplete = new();
|
||||
_hits = [];
|
||||
_armPosition = 0f;
|
||||
_armSpeed = 1f;
|
||||
_regions = [];
|
||||
|
||||
SetupRegions(builder);
|
||||
|
||||
_maxHitCount = builder.maxHitCount;
|
||||
}
|
||||
|
||||
private void SetupRegions<T>(Builder<T> builder)
|
||||
{
|
||||
// common calculations
|
||||
var totalRegionProportion = builder.regions.Sum(r => r.proportion);
|
||||
|
||||
// spawn regions
|
||||
var regionSum = 0f;
|
||||
foreach (var region in builder.regions)
|
||||
{
|
||||
var regionVisual = _regionVisualPrefab.Instantiate<RegionVisual>();
|
||||
_regionsParent.AddChild(regionVisual);
|
||||
|
||||
var normalisedAngleStart = regionSum / totalRegionProportion;
|
||||
var normalisedAngleEnd = (regionSum + region.proportion) / totalRegionProportion;
|
||||
var normalAngles = new Vector2(normalisedAngleStart, normalisedAngleEnd);
|
||||
|
||||
regionVisual.Setup(normalAngles, _baseRegionColor.RandomHue(), region.text, region.theme);
|
||||
|
||||
regionSum += region.proportion;
|
||||
|
||||
_regions!.Add(normalisedAngleEnd);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowMinigame()
|
||||
{
|
||||
Show();
|
||||
ProcessMode = ProcessModeEnum.Inherit;
|
||||
}
|
||||
|
||||
private void HideMinigame()
|
||||
{
|
||||
Hide();
|
||||
ProcessMode = ProcessModeEnum.Disabled;
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_minigameComplete = null;
|
||||
_hits = null;
|
||||
_regionsParent.GetChildren().ForEach(c => c.QueueFree());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ct7l4er2kljnc
|
||||
@@ -0,0 +1,32 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using Babushka.scripts.CSharp.Common.Minigame;
|
||||
using Godot.Collections;
|
||||
|
||||
public partial class RegionVisual : Node
|
||||
{
|
||||
[Export] private Sprite2D _sliceSprite;
|
||||
[Export] private Label _textLabel;
|
||||
[Export] private Node2D _labelPivot;
|
||||
|
||||
[Export(PropertyHint.DictionaryType)] private Dictionary<MinigameController.RegionTheme, Color> _fillColors = new();
|
||||
|
||||
public void Setup(Vector2 normalAngles, Color color, string regionText, MinigameController.RegionTheme regionTheme)
|
||||
{
|
||||
var mat = (_sliceSprite.Material as ShaderMaterial)!;
|
||||
mat.SetShaderParameter("angles", normalAngles);
|
||||
mat.SetShaderParameter("fillColor", GetFillColor(regionTheme));
|
||||
|
||||
var averageAngleRadians = (normalAngles.X + normalAngles.Y) * float.Pi; // '/ 2' from the average and '* 2' from the radians cancel out
|
||||
_labelPivot.Rotation = averageAngleRadians;
|
||||
_textLabel.Rotation = -averageAngleRadians;
|
||||
|
||||
_textLabel.Text = regionText;
|
||||
}
|
||||
|
||||
private Color GetFillColor(MinigameController.RegionTheme theme)
|
||||
{
|
||||
if (_fillColors.TryGetValue(theme, out var color)) return color;
|
||||
throw new InvalidOperationException($"No fill color for theme {theme}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdpsa4qrlai31
|
||||
@@ -0,0 +1,10 @@
|
||||
using Godot;
|
||||
using System;
|
||||
|
||||
public partial class SpinnyArmVisual : Node2D
|
||||
{
|
||||
public void SetAngle(float normalAngle)
|
||||
{
|
||||
Rotation = normalAngle * float.Pi * 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://djkyrp24ljff0
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Babushka.scripts.CSharp.Common.Minigame;
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.TestScripts;
|
||||
|
||||
public partial class MinigameTestStarter : Node
|
||||
{
|
||||
[Export] private MinigameController _minigameController = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_minigameController.Run(
|
||||
new MinigameController.Builder<int>()
|
||||
.WithHitCount(5)
|
||||
.AddRegion(1)
|
||||
.AddRegion(2).RegionWithProportion(2)
|
||||
.AddRegion(3)
|
||||
.AddRegion(4).RegionWithProportion(2)
|
||||
.AddRegion(5)
|
||||
.AddRegion(6).RegionWithProportion(2)
|
||||
).ContinueWith(task => OnEnd(task.Result));
|
||||
}
|
||||
|
||||
private void OnEnd(List<int> result)
|
||||
{
|
||||
GD.Print(string.Join(" ,", result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://iv0dbf32bfw1
|
||||
@@ -0,0 +1,16 @@
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Util;
|
||||
|
||||
public partial class ClickDetect : Area2D
|
||||
{
|
||||
[Signal] public delegate void ClickEventHandler();
|
||||
|
||||
public override void _Input(InputEvent evt)
|
||||
{
|
||||
if (evt is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true })
|
||||
{
|
||||
EmitSignalClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dq7gahfp0lk7v
|
||||
@@ -4,6 +4,8 @@ using System.Collections.Generic;
|
||||
using System.Data.SqlTypes;
|
||||
using System.Linq;
|
||||
using System.Xml.Schema;
|
||||
using Godot;
|
||||
|
||||
namespace Babushka.scripts.CSharp.Common.Util;
|
||||
|
||||
public static class LinqExtras
|
||||
@@ -16,6 +18,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 +36,23 @@ 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;
|
||||
}
|
||||
|
||||
public static Color RandomHue(this Color color)
|
||||
{
|
||||
color.ToHsv(out _, out float saturation, out var value );
|
||||
var rng = new RandomNumberGenerator();
|
||||
return Color.FromHsv(rng.Randf(), saturation, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bxs7sn7j3vd0n
|
||||
Reference in New Issue
Block a user