In progress refactoring of Boss class

This commit is contained in:
2025-02-18 01:55:06 -08:00
parent 368e774176
commit 5ce65aa57a
411 changed files with 2011 additions and 829 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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>();
}

View File

@@ -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

View 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
{
}
}
}

View File

@@ -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>();
}
}
}

View File

@@ -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();
}
}

View 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());
}
}
}
}

View File

@@ -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();
}
}
}
}

View 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();
}
}
}
}

View File

@@ -16,7 +16,6 @@ public partial class BossLogic
var boss = Get<IBoss>();
boss.SetPhysicsProcess(true);
boss.Show();
boss.AttackTimer.Start();
}
);
}

View 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>();
}
}
}
}