In progress refactoring of Boss class
This commit is contained in:
182
src/boss/Boss.cs
182
src/boss/Boss.cs
@@ -8,15 +8,19 @@ namespace GameJamDungeon;
|
||||
|
||||
public interface IBoss : ICharacterBody3D
|
||||
{
|
||||
public AnimationTree AnimationTree { get; }
|
||||
|
||||
public AnimationPlayer HitAnimation { get; }
|
||||
|
||||
public Timer AttackTimer { get; }
|
||||
|
||||
public void Activate();
|
||||
|
||||
public AutoProp<double> CurrentHP { get; }
|
||||
public void TakeDamage(double damage, ElementType elementType = ElementType.None, bool isCriticalHit = false, bool ignoreDefense = false, bool ignoreElementalResistance = false);
|
||||
|
||||
public void MoveToLocation(Vector3 target, float delta);
|
||||
|
||||
public void StartAttackTimer();
|
||||
|
||||
public void StopAttackTimer();
|
||||
|
||||
public double CurrentHP { get; }
|
||||
|
||||
public AutoProp<bool> IsDefeated { get; }
|
||||
}
|
||||
|
||||
[Meta(typeof(IAutoNode))]
|
||||
@@ -24,39 +28,47 @@ public partial class Boss : CharacterBody3D, IBoss, IProvide<IBossLogic>
|
||||
{
|
||||
public override void _Notification(int what) => this.Notify(what);
|
||||
|
||||
public IBossLogic BossLogic { get; set; } = default!;
|
||||
public double CurrentHP => _currentHP.Value;
|
||||
|
||||
[Export] public EnemyStatResource BossResource { get; set; } = default!;
|
||||
public AutoProp<bool> IsDefeated { get; set; }
|
||||
|
||||
IBossLogic IProvide<IBossLogic>.Value() => BossLogic;
|
||||
private AutoProp<double> _currentHP { get; set; }
|
||||
|
||||
#region Autoinject
|
||||
protected IBossLogic _bossLogic { get; set; } = default!;
|
||||
|
||||
IBossLogic IProvide<IBossLogic>.Value() => _bossLogic;
|
||||
|
||||
public BossLogic.IBinding BossBinding { get; set; } = default!;
|
||||
#endregion
|
||||
|
||||
[Dependency] public IGameEventDepot GameEventDepot => this.DependOn<IGameEventDepot>();
|
||||
#region Export
|
||||
[Export] private EnemyStatResource _bossStatResource { get; set; } = default!;
|
||||
#endregion
|
||||
|
||||
[Dependency] public IGame Game => this.DependOn<IGame>();
|
||||
#region Dependencies
|
||||
[Dependency] private IPlayer _player => this.DependOn<IPlayer>();
|
||||
#endregion
|
||||
|
||||
[Dependency] public IPlayer Player => this.DependOn<IPlayer>();
|
||||
#region Nodes
|
||||
[Node] private AnimationTree _animationTree { get; set; } = default!;
|
||||
|
||||
[Node] public AnimationTree AnimationTree { get; set; } = default!;
|
||||
[Node] private Timer _attackTimer { get; set; } = default!;
|
||||
|
||||
[Node] public Timer AttackTimer { get; set; } = default!;
|
||||
[Node] private AnimationPlayer _hitAnimation { get; set; } = default!;
|
||||
|
||||
[Node] public AnimationPlayer HitAnimation { get; set; } = default!;
|
||||
[Node] private Area3D _hitbox { get; set; } = default!;
|
||||
|
||||
[Node] public Area3D Hitbox { get; set; } = default!;
|
||||
[Node] private Area3D _attackBox { get; set; } = default!;
|
||||
|
||||
[Node] public Area3D AttackBox { get; set; } = default!;
|
||||
|
||||
[Node] public Area3D SecondaryAttackBox { get; set; } = default!;
|
||||
|
||||
public AutoProp<double> CurrentHP { get; set; }
|
||||
[Node] private Area3D _secondaryAttackBox { get; set; } = default!;
|
||||
#endregion
|
||||
|
||||
public void Setup()
|
||||
{
|
||||
BossLogic = new BossLogic();
|
||||
BossLogic.Set(this as IBoss);
|
||||
BossLogic.Set(Player);
|
||||
_bossLogic = new BossLogic();
|
||||
_bossLogic.Set(this as IBoss);
|
||||
_bossLogic.Set(_player);
|
||||
|
||||
SetPhysicsProcess(false);
|
||||
Hide();
|
||||
@@ -64,87 +76,87 @@ public partial class Boss : CharacterBody3D, IBoss, IProvide<IBossLogic>
|
||||
|
||||
public void OnResolved()
|
||||
{
|
||||
BossBinding = BossLogic.Bind();
|
||||
BossBinding = _bossLogic.Bind();
|
||||
BossBinding
|
||||
.Handle((in BossLogic.Output.Defeated output) =>
|
||||
{
|
||||
HitAnimation.Play("Defeated");
|
||||
_hitAnimation.Play("Defeated");
|
||||
});
|
||||
this.Provide();
|
||||
BossLogic.Start();
|
||||
CurrentHP = new AutoProp<double>(BossResource.MaximumHP);
|
||||
CurrentHP.Sync += OnHPChanged;
|
||||
AttackTimer.Timeout += AttackTimer_Timeout;
|
||||
Hitbox.AreaEntered += Hitbox_AreaEntered;
|
||||
HitAnimation.AnimationFinished += HitAnimation_AnimationFinished;
|
||||
AttackBox.AreaEntered += AttackBox_AreaEntered;
|
||||
SecondaryAttackBox.AreaEntered += SecondaryAttackBox_AreaEntered;
|
||||
}
|
||||
|
||||
private void AttackBox_AreaEntered(Area3D area)
|
||||
{
|
||||
//var bossHitDamage = DamageCalculator.CalculateEnemyAttackDamage(GameRepo.PlayerData.CurrentDefense.Value + GameRepo.PlayerData.BonusDefense,
|
||||
// BossResource,
|
||||
// GameRepo.PlayerData.Inventory.EquippedArmor.Value.ArmorStats,
|
||||
// false);
|
||||
//GameRepo.PlayerData.SetCurrentHP(GameRepo.PlayerData.CurrentHP.Value - Mathf.RoundToInt(bossHitDamage));
|
||||
}
|
||||
|
||||
private void SecondaryAttackBox_AreaEntered(Area3D area)
|
||||
{
|
||||
//var bossHitDamage = DamageCalculator.CalculateEnemyAttackDamage(GameRepo.PlayerData.CurrentDefense.Value + GameRepo.PlayerData.BonusDefense,
|
||||
// BossResource,
|
||||
// GameRepo.PlayerData.Inventory.EquippedArmor.Value.ArmorStats,
|
||||
// false);
|
||||
//var nerfDamage = bossHitDamage *= 0.25f;
|
||||
//GameRepo.PlayerData.SetCurrentHP(GameRepo.PlayerData.CurrentHP.Value - Mathf.RoundToInt(nerfDamage));
|
||||
Player.Knockback(115f);
|
||||
}
|
||||
|
||||
private void HitAnimation_AnimationFinished(StringName animName)
|
||||
{
|
||||
if (animName == "Defeated")
|
||||
QueueFree();
|
||||
}
|
||||
|
||||
private void Hitbox_AreaEntered(Area3D area)
|
||||
{
|
||||
if (area is IHitbox)
|
||||
{
|
||||
var isCriticalHit = false;
|
||||
var rng = new RandomNumberGenerator();
|
||||
rng.Randomize();
|
||||
var roll = rng.Randf();
|
||||
if (roll <= Player.Inventory.EquippedWeapon.Value.Luck)
|
||||
isCriticalHit = true;
|
||||
}
|
||||
_bossLogic.Start();
|
||||
_currentHP = new AutoProp<double>(_bossStatResource.MaximumHP);
|
||||
_currentHP.Sync += OnHPChanged;
|
||||
_attackTimer.Timeout += AttackTimer_Timeout;
|
||||
}
|
||||
|
||||
public void Activate()
|
||||
{
|
||||
BossLogic.Input(new BossLogic.Input.Activate());
|
||||
_bossLogic.Input(new BossLogic.Input.Activate());
|
||||
}
|
||||
|
||||
private void AttackTimer_Timeout()
|
||||
public void TakeDamage(double damage, ElementType elementType, bool isCriticalHit = false, bool ignoreDefense = false, bool ignoreElementalResistance = false)
|
||||
{
|
||||
if (_currentHP.Value > 0)
|
||||
{
|
||||
if (!ignoreElementalResistance)
|
||||
damage = CalculateElementalResistance(damage, elementType);
|
||||
if (!ignoreDefense)
|
||||
damage = CalculateDefenseResistance(damage);
|
||||
if (isCriticalHit)
|
||||
damage *= 2;
|
||||
GD.Print($"Enemy Hit for {damage} damage.");
|
||||
_currentHP.OnNext(_currentHP.Value - damage);
|
||||
GD.Print("Current HP: " + _currentHP.Value);
|
||||
|
||||
if (_currentHP.Value <= 0)
|
||||
return;
|
||||
|
||||
//EnemyModelView.PlayHitAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
public void MoveToLocation(Vector3 target, float delta) => throw new System.NotImplementedException();
|
||||
|
||||
public void StartAttackTimer() => _attackTimer.Start();
|
||||
|
||||
public void StopAttackTimer() => _attackTimer.Stop();
|
||||
|
||||
private void AttackTimer_Timeout() => _bossLogic.Input(new BossLogic.Input.AttackTimer());
|
||||
|
||||
public virtual void TakeAction()
|
||||
{
|
||||
var random = new RandomNumberGenerator();
|
||||
random.Randomize();
|
||||
var selection = random.RandWeighted([0.2f, 0.8f]);
|
||||
if (selection == 0)
|
||||
BossLogic.Input(new BossLogic.Input.SecondaryAttack());
|
||||
else
|
||||
BossLogic.Input(new BossLogic.Input.PrimaryAttack());
|
||||
}
|
||||
|
||||
public void OnPhysicsProcess(double delta)
|
||||
{
|
||||
BossLogic.Input(new BossLogic.Input.PhysicsTick(delta));
|
||||
_bossLogic.Input(new BossLogic.Input.PhysicsTick(delta));
|
||||
MoveAndSlide();
|
||||
}
|
||||
|
||||
private void OnHPChanged(double newHP)
|
||||
{
|
||||
if (newHP <= 0)
|
||||
BossLogic.Input(new BossLogic.Input.BossDefeated());
|
||||
_bossLogic.Input(new BossLogic.Input.BossDefeated());
|
||||
}
|
||||
|
||||
private double CalculateElementalResistance(double incomingDamage, ElementType incomingElementType)
|
||||
{
|
||||
if (incomingElementType == ElementType.Aeolic)
|
||||
return Mathf.Max(incomingDamage - (incomingDamage * _bossStatResource.AeolicResistance), 0.0);
|
||||
if (incomingElementType == ElementType.Hydric)
|
||||
return Mathf.Max(incomingDamage - (incomingDamage * _bossStatResource.HydricResistance), 0.0);
|
||||
if (incomingElementType == ElementType.Igneous)
|
||||
return Mathf.Max(incomingDamage - (incomingDamage * _bossStatResource.IgneousResistance), 0.0);
|
||||
if (incomingElementType == ElementType.Ferrum)
|
||||
return Mathf.Max(incomingDamage - (incomingDamage * _bossStatResource.FerrumResistance), 0.0);
|
||||
if (incomingElementType == ElementType.Telluric)
|
||||
return Mathf.Max(incomingDamage - (incomingDamage * _bossStatResource.TelluricResistance), 0.0);
|
||||
|
||||
return Mathf.Max(incomingDamage, 0.0);
|
||||
}
|
||||
|
||||
private double CalculateDefenseResistance(double incomingDamage)
|
||||
{
|
||||
return Mathf.Max(incomingDamage - _bossStatResource.CurrentDefense, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ public partial class BossLogic
|
||||
{
|
||||
public static class Input
|
||||
{
|
||||
public readonly record struct Activate();
|
||||
public readonly record struct Activate;
|
||||
|
||||
public readonly record struct PhysicsTick(double Delta);
|
||||
|
||||
public readonly record struct PrimaryAttack();
|
||||
public readonly record struct StopMoving;
|
||||
|
||||
public readonly record struct SecondaryAttack();
|
||||
public readonly record struct AttackTimer;
|
||||
|
||||
public readonly record struct HitByPlayer(double Damage);
|
||||
public readonly record struct StartAttacking;
|
||||
|
||||
public readonly record struct BossDefeated();
|
||||
public readonly record struct BossDefeated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
namespace GameJamDungeon;
|
||||
using Godot;
|
||||
|
||||
namespace GameJamDungeon;
|
||||
|
||||
public partial class BossLogic
|
||||
{
|
||||
public static class Output
|
||||
{
|
||||
public readonly record struct HitByPlayer(double CurrentHP);
|
||||
public readonly record struct MoveTowardsPlayer(Vector3 TargetPosition);
|
||||
|
||||
public readonly record struct Defeated();
|
||||
public readonly record struct MovementComputed(Vector3 LinearVelocity);
|
||||
|
||||
public readonly record struct TakeAction;
|
||||
|
||||
public readonly record struct Defeated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ public interface IBossLogic : ILogicBlock<BossLogic.State>;
|
||||
[LogicBlock(typeof(State), Diagram = true)]
|
||||
public partial class BossLogic : LogicBlock<BossLogic.State>, IBossLogic
|
||||
{
|
||||
public override Transition GetInitialState() => To<State.Idle>();
|
||||
public override Transition GetInitialState() => To<State.Unactivated>();
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
@startuml BossLogic
|
||||
state "BossLogic State" as GameJamDungeon_BossLogic_State {
|
||||
state "Defeated" as GameJamDungeon_BossLogic_State_Defeated
|
||||
state "Alive" as GameJamDungeon_BossLogic_State_Alive {
|
||||
state "Idle" as GameJamDungeon_BossLogic_State_Idle
|
||||
state "EngagePlayer" as GameJamDungeon_BossLogic_State_EngagePlayer
|
||||
state "ApproachPlayer" as GameJamDungeon_BossLogic_State_ApproachPlayer
|
||||
state "Activated" as GameJamDungeon_BossLogic_State_Activated {
|
||||
state "Attacking" as GameJamDungeon_BossLogic_State_Attacking
|
||||
state "FollowPlayer" as GameJamDungeon_BossLogic_State_FollowPlayer
|
||||
}
|
||||
}
|
||||
state "ApproachPlayer" as GameJamDungeon_BossLogic_State_ApproachPlayer
|
||||
state "Defeated" as GameJamDungeon_BossLogic_State_Defeated
|
||||
state "EngagePlayer" as GameJamDungeon_BossLogic_State_EngagePlayer
|
||||
}
|
||||
|
||||
GameJamDungeon_BossLogic_State_Alive --> GameJamDungeon_BossLogic_State_Alive : HitByPlayer
|
||||
GameJamDungeon_BossLogic_State_Alive --> GameJamDungeon_BossLogic_State_Defeated : BossDefeated
|
||||
GameJamDungeon_BossLogic_State_ApproachPlayer --> GameJamDungeon_BossLogic_State_ApproachPlayer : PhysicsTick
|
||||
GameJamDungeon_BossLogic_State_ApproachPlayer --> GameJamDungeon_BossLogic_State_EngagePlayer : PhysicsTick
|
||||
GameJamDungeon_BossLogic_State_EngagePlayer --> GameJamDungeon_BossLogic_State_ApproachPlayer : PhysicsTick
|
||||
GameJamDungeon_BossLogic_State_EngagePlayer --> GameJamDungeon_BossLogic_State_EngagePlayer : PhysicsTick
|
||||
GameJamDungeon_BossLogic_State_EngagePlayer --> GameJamDungeon_BossLogic_State_EngagePlayer : PrimaryAttack
|
||||
GameJamDungeon_BossLogic_State_EngagePlayer --> GameJamDungeon_BossLogic_State_EngagePlayer : SecondaryAttack
|
||||
GameJamDungeon_BossLogic_State_FollowPlayer --> GameJamDungeon_BossLogic_State_FollowPlayer : PhysicsTick
|
||||
GameJamDungeon_BossLogic_State_Idle --> GameJamDungeon_BossLogic_State_ApproachPlayer : Activate
|
||||
|
||||
GameJamDungeon_BossLogic_State : On() → HitByPlayer
|
||||
GameJamDungeon_BossLogic_State_Alive : OnHitByPlayer → HitByPlayer
|
||||
GameJamDungeon_BossLogic_State : On() → Defeated
|
||||
GameJamDungeon_BossLogic_State_Alive : OnBossDefeated → Defeated
|
||||
|
||||
[*] --> GameJamDungeon_BossLogic_State_Idle
|
||||
@enduml
|
||||
14
src/boss/state/states/BossLogic.State.Activated.cs
Normal file
14
src/boss/state/states/BossLogic.State.Activated.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Chickensoft.Introspection;
|
||||
|
||||
namespace GameJamDungeon;
|
||||
|
||||
public partial class BossLogic
|
||||
{
|
||||
public partial record State
|
||||
{
|
||||
[Meta, Id("boss_logic_state_activated")]
|
||||
public partial record Activated : Alive
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,14 @@ public partial class BossLogic
|
||||
public partial record State
|
||||
{
|
||||
[Meta, Id("boss_logic_state_alive")]
|
||||
public abstract partial record Alive : State, IGet<Input.HitByPlayer>, IGet<Input.BossDefeated>
|
||||
public abstract partial record Alive : State, IGet<Input.BossDefeated>, IGet<Input.AttackTimer>, IGet<Input.StopMoving>, IGet<Input.StartAttacking>
|
||||
{
|
||||
}
|
||||
|
||||
public Transition On(in Input.HitByPlayer input)
|
||||
public Transition On(in Input.AttackTimer input)
|
||||
{
|
||||
var enemy = Get<IBoss>();
|
||||
enemy.HitAnimation.Play("Hit");
|
||||
enemy.CurrentHP.OnNext(enemy.CurrentHP.Value - input.Damage);
|
||||
GD.Print("Current HP: " + enemy.CurrentHP.Value);
|
||||
Output(new Output.HitByPlayer());
|
||||
return ToSelf();
|
||||
Output(new Output.TakeAction());
|
||||
return To<Attacking>();
|
||||
}
|
||||
|
||||
public Transition On(in Input.BossDefeated input)
|
||||
@@ -27,5 +23,15 @@ public partial class BossLogic
|
||||
Output(new Output.Defeated());
|
||||
return To<Defeated>();
|
||||
}
|
||||
|
||||
public Transition On(in Input.StopMoving input)
|
||||
{
|
||||
return To<Idle>();
|
||||
}
|
||||
|
||||
public Transition On(in Input.StartAttacking input)
|
||||
{
|
||||
return To<Attacking>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ public partial class BossLogic
|
||||
|
||||
var targetDirection = boss.GlobalPosition - player.CurrentPosition;
|
||||
boss.GlobalRotation = new Vector3(boss.GlobalRotation.X, Mathf.LerpAngle(boss.GlobalRotation.Y, Mathf.Atan2(-targetDirection.X, -targetDirection.Z), delta * 3f), boss.GlobalRotation.Z);
|
||||
boss.AnimationTree.Get("parameters/playback").As<AnimationNodeStateMachinePlayback>().Travel("Walk");
|
||||
return ToSelf();
|
||||
}
|
||||
}
|
||||
|
||||
19
src/boss/state/states/BossLogic.State.Attacking.cs
Normal file
19
src/boss/state/states/BossLogic.State.Attacking.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Chickensoft.Introspection;
|
||||
|
||||
namespace GameJamDungeon;
|
||||
|
||||
public partial class BossLogic
|
||||
{
|
||||
public partial record State
|
||||
{
|
||||
[Meta, Id("boss_logic_state_attacking")]
|
||||
public partial record Attacking : Activated, IGet<Input.StopMoving>
|
||||
{
|
||||
public Attacking()
|
||||
{
|
||||
OnAttach(() => Get<IEnemy>().StartAttackTimer());
|
||||
OnDetach(() => Get<IEnemy>().StopAttackTimer());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,10 @@ public partial class BossLogic
|
||||
public partial record State
|
||||
{
|
||||
[Meta, Id("boss_logic_state_engage_player")]
|
||||
public partial record EngagePlayer : Alive, IGet<Input.PhysicsTick>, IGet<Input.PrimaryAttack>, IGet<Input.SecondaryAttack>
|
||||
public partial record EngagePlayer : Alive, IGet<Input.PhysicsTick>
|
||||
{
|
||||
public EngagePlayer()
|
||||
{
|
||||
OnAttach(() => Get<IBoss>().AnimationTree.Get("parameters/playback").As<AnimationNodeStateMachinePlayback>().Travel("Idle"));
|
||||
}
|
||||
|
||||
public Transition On(in Input.PhysicsTick input)
|
||||
@@ -28,20 +27,6 @@ public partial class BossLogic
|
||||
|
||||
return ToSelf();
|
||||
}
|
||||
|
||||
public Transition On(in Input.PrimaryAttack input)
|
||||
{
|
||||
var boss = Get<IBoss>();
|
||||
boss.AnimationTree.Get("parameters/playback").As<AnimationNodeStateMachinePlayback>().Travel("PrimaryAttack");
|
||||
return ToSelf();
|
||||
}
|
||||
|
||||
public Transition On(in Input.SecondaryAttack input)
|
||||
{
|
||||
var boss = Get<IBoss>();
|
||||
boss.AnimationTree.Get("parameters/playback").As<AnimationNodeStateMachinePlayback>().Travel("SecondaryAttack");
|
||||
return ToSelf();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
src/boss/state/states/BossLogic.State.FollowPlayer.cs
Normal file
23
src/boss/state/states/BossLogic.State.FollowPlayer.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Chickensoft.Introspection;
|
||||
|
||||
namespace GameJamDungeon;
|
||||
|
||||
public partial class BossLogic
|
||||
{
|
||||
public partial record State
|
||||
{
|
||||
[Meta, Id("boss_logic_state_followplayer")]
|
||||
public partial record FollowPlayer : Activated, IGet<Input.PhysicsTick>
|
||||
{
|
||||
public Transition On(in Input.PhysicsTick input)
|
||||
{
|
||||
var delta = input.Delta;
|
||||
var enemy = Get<IEnemy>();
|
||||
var player = Get<IPlayer>();
|
||||
var target = player.CurrentPosition;
|
||||
enemy.MoveToLocation(target, (float)delta);
|
||||
return ToSelf();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ public partial class BossLogic
|
||||
var boss = Get<IBoss>();
|
||||
boss.SetPhysicsProcess(true);
|
||||
boss.Show();
|
||||
boss.AttackTimer.Start();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
18
src/boss/state/states/BossLogic.State.Unactivated.cs
Normal file
18
src/boss/state/states/BossLogic.State.Unactivated.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Chickensoft.Introspection;
|
||||
|
||||
namespace GameJamDungeon;
|
||||
|
||||
public partial class BossLogic
|
||||
{
|
||||
public partial record State
|
||||
{
|
||||
[Meta, Id("boss_logic_state_unactivated")]
|
||||
public partial record Unactivated : State, IGet<Input.Activate>
|
||||
{
|
||||
public Transition On(in Input.Activate input)
|
||||
{
|
||||
return To<Activated>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user