422 lines
12 KiB
C#
422 lines
12 KiB
C#
using Chickensoft.AutoInject;
|
|
using Chickensoft.GodotNodeInterfaces;
|
|
using Chickensoft.Introspection;
|
|
using Godot;
|
|
using SimpleInjector;
|
|
using System;
|
|
using Zennysoft.Ma.Adapter;
|
|
using Zennysoft.Ma.Adapter.Entity;
|
|
|
|
namespace Zennysoft.Game.Ma;
|
|
|
|
[Meta(typeof(IAutoNode))]
|
|
public partial class Player : CharacterBody3D, IPlayer, IProvide<IPlayer>
|
|
{
|
|
#region Dependency Injection
|
|
public override void _Notification(int what) => this.Notify(what);
|
|
|
|
IPlayer IProvide<IPlayer>.Value() => this;
|
|
|
|
private PlayerLogic.IBinding PlayerBinding { get; set; } = default!;
|
|
#endregion
|
|
|
|
public IHealthComponent HealthComponent { get; private set; }
|
|
|
|
public IVTComponent VTComponent { get; private set; }
|
|
|
|
public IAttackComponent AttackComponent { get; private set; }
|
|
|
|
public IDefenseComponent DefenseComponent { get; private set; }
|
|
|
|
public IExperiencePointsComponent ExperiencePointsComponent { get; private set; }
|
|
|
|
public ILuckComponent LuckComponent { get; private set; }
|
|
|
|
public IEquipmentComponent EquipmentComponent { get; private set; }
|
|
|
|
public Vector3 CurrentPosition => GlobalPosition;
|
|
|
|
public Basis CurrentBasis => Transform.Basis;
|
|
|
|
public IInventory Inventory { get; private set; } = default!;
|
|
|
|
public event Action PlayerDied;
|
|
|
|
private PlayerLogic.Settings Settings { get; set; } = default!;
|
|
|
|
private IPlayerLogic PlayerLogic { get; set; } = default!;
|
|
|
|
#region Exports
|
|
[ExportGroup("Movement")]
|
|
[Export(PropertyHint.Range, "0, 100, 0.1")]
|
|
public float RotationSpeed { get; set; } = 1.5f;
|
|
|
|
[Export(PropertyHint.Range, "0, 100, 0.1")]
|
|
public float MoveSpeed { get; set; } = 4f;
|
|
|
|
[Export(PropertyHint.Range, "0, 100, 0.1")]
|
|
public float Acceleration { get; set; } = 2f;
|
|
|
|
[ExportGroup("Player Stats")]
|
|
[Export(PropertyHint.Range, "1, 1000, 1")] public int InitialHP { get; set; } = 212;
|
|
[Export(PropertyHint.Range, "0, 1000, 1")] public int InitialVT { get; set; } = 116;
|
|
|
|
[Export(PropertyHint.Range, "1, 100, 1")] public int InitialAttack { get; set; } = 16;
|
|
|
|
[Export(PropertyHint.Range, "1, 100, 1")] public int InitialDefense { get; set; } = 12;
|
|
|
|
[Export(PropertyHint.Range, "1, 100, 1")] public int InitialLuck { get; set; } = 8;
|
|
|
|
[Export]
|
|
private bool HealthTimerIsActive = false;
|
|
#endregion
|
|
|
|
#region Node Dependencies
|
|
[Node] private IAnimationPlayer AnimationPlayer { get; set; } = default!;
|
|
|
|
[Node] private Area3D Hitbox { get; set; } = default!;
|
|
|
|
[Node] private Area3D CollisionDetector { get; set; } = default!;
|
|
|
|
[Node] private Timer HealthTimer { get; set; } = default!;
|
|
|
|
[Node] private RigidBody3D WallCheck { get; set; } = default!;
|
|
|
|
[Node] private AudioStreamPlayer3D WalkSFX { get; set; } = default!;
|
|
#endregion
|
|
|
|
|
|
private bool flipAttack = false;
|
|
|
|
private float _healthTimerWaitTime = 3.0f;
|
|
|
|
private bool reduceOnTick = true;
|
|
|
|
private float _knockbackStrength = 0.0f;
|
|
private Vector3 _knockbackDirection = Vector3.Zero;
|
|
|
|
private ItemReroller _itemReroller;
|
|
|
|
public void Initialize()
|
|
{
|
|
var container = new SimpleInjector.Container();
|
|
container.Register<IPlayerLogic, PlayerLogic>(Lifestyle.Singleton);
|
|
|
|
PlayerLogic = container.GetInstance<IPlayerLogic>();
|
|
PlayerLogic.Set(this as IPlayer);
|
|
PlayerLogic.Set(Settings);
|
|
|
|
Inventory = new Inventory();
|
|
HealthComponent = new HealthComponent(InitialHP);
|
|
VTComponent = new VTComponent(InitialVT);
|
|
AttackComponent = new AttackComponent(InitialAttack);
|
|
DefenseComponent = new DefenseComponent(InitialDefense);
|
|
ExperiencePointsComponent = new ExperiencePointsComponent();
|
|
LuckComponent = new LuckComponent(InitialLuck);
|
|
EquipmentComponent = new EquipmentComponent();
|
|
|
|
_itemReroller = new ItemReroller(ItemDatabase.Instance);
|
|
|
|
Settings = new PlayerLogic.Settings() { RotationSpeed = RotationSpeed, MoveSpeed = MoveSpeed, Acceleration = Acceleration };
|
|
|
|
PlayerBinding = PlayerLogic.Bind();
|
|
|
|
PlayerBinding
|
|
.Handle((in PlayerLogic.Output.ThrowItem output) =>
|
|
{
|
|
})
|
|
.Handle((in PlayerLogic.Output.Move output) =>
|
|
{
|
|
Move(output.delta);
|
|
});
|
|
|
|
PlayerLogic.Start();
|
|
this.Provide();
|
|
}
|
|
|
|
public void ResetPlayerData()
|
|
{
|
|
foreach (var item in Inventory.Items)
|
|
Inventory.Remove(item);
|
|
|
|
HealthComponent.Reset();
|
|
VTComponent.Reset();
|
|
AttackComponent.Reset();
|
|
DefenseComponent.Reset();
|
|
ExperiencePointsComponent.Reset();
|
|
LuckComponent.Reset();
|
|
EquipmentComponent.Reset();
|
|
|
|
HealthTimer.Timeout += OnHealthTimerTimeout;
|
|
}
|
|
|
|
#region Initialization
|
|
public void OnReady()
|
|
{
|
|
Hitbox.AreaEntered += Hitbox_AreaEntered;
|
|
CollisionDetector.AreaEntered += CollisionDetector_AreaEntered;
|
|
HealthComponent.HealthReachedZero += Die;
|
|
HealthTimer.WaitTime = _healthTimerWaitTime;
|
|
SetProcessInput(false);
|
|
SetPhysicsProcess(false);
|
|
}
|
|
#endregion
|
|
|
|
public void Activate()
|
|
{
|
|
SetProcessInput(true);
|
|
SetPhysicsProcess(true);
|
|
SetHealthTimerStatus(HealthTimerIsActive);
|
|
}
|
|
|
|
public void Deactivate()
|
|
{
|
|
SetProcessInput(false);
|
|
SetPhysicsProcess(false);
|
|
SetHealthTimerStatus(false);
|
|
}
|
|
|
|
private void SetHealthTimerStatus(bool isActive)
|
|
{
|
|
if (isActive)
|
|
HealthTimer.Start();
|
|
else
|
|
HealthTimer.Stop();
|
|
}
|
|
|
|
public void TeleportPlayer(Transform3D newTransform)
|
|
{
|
|
Transform = newTransform;
|
|
}
|
|
|
|
public void TakeDamage(AttackData damage)
|
|
{
|
|
var damageReceived = DamageCalculator.CalculateDamage(damage, DefenseComponent.CurrentDefense.Value + EquipmentComponent.BonusDefense, EquipmentComponent.ElementalResistance);
|
|
HealthComponent.Damage(damageReceived);
|
|
SfxDatabase.Instance.Play(SoundEffect.TakeDamage);
|
|
}
|
|
|
|
public void Knockback(float impulse)
|
|
{
|
|
_knockbackStrength = impulse;
|
|
_knockbackDirection = GlobalBasis.Z.Normalized();
|
|
}
|
|
|
|
public void LevelUp()
|
|
{
|
|
var rng = new RandomNumberGenerator();
|
|
rng.Randomize();
|
|
var hpIncrease = rng.RandiRange(3, 6);
|
|
HealthComponent.RaiseMaximumHP(hpIncrease);
|
|
ExperiencePointsComponent.LevelUp();
|
|
}
|
|
|
|
public void Die()
|
|
{
|
|
HealthTimer.WaitTime = _healthTimerWaitTime;
|
|
HealthTimer.Timeout -= OnHealthTimerTimeout;
|
|
SetProcessInput(false);
|
|
SetPhysicsProcess(false);
|
|
PlayerDied?.Invoke();
|
|
}
|
|
|
|
public override void _Input(InputEvent @event)
|
|
{
|
|
if (@event.IsActionPressed(GameInputs.Attack))
|
|
Attack();
|
|
if (@event.IsActionPressed(GameInputs.Sprint))
|
|
Settings.MoveSpeed *= 2;
|
|
if (@event.IsActionReleased(GameInputs.Sprint))
|
|
Settings.MoveSpeed /= 2;
|
|
}
|
|
|
|
public void OnPhysicsProcess(double delta)
|
|
{
|
|
PlayerLogic.Input(new PlayerLogic.Input.PhysicsTick(delta));
|
|
PlayerLogic.Input(new PlayerLogic.Input.Moved(GlobalPosition, GlobalTransform));
|
|
}
|
|
|
|
public void Equip(EquipableItem equipable)
|
|
{
|
|
if (equipable.ItemTag == ItemTag.MysteryItem)
|
|
{
|
|
var rerolledItem = _itemReroller.RerollItem(equipable, Inventory);
|
|
Equip(rerolledItem);
|
|
return;
|
|
}
|
|
|
|
HealthComponent.RaiseMaximumHP(equipable.BonusHP, false);
|
|
VTComponent.RaiseMaximumVT(equipable.BonusVT, false);
|
|
|
|
EquipmentComponent.Equip(equipable);
|
|
}
|
|
|
|
public void Unequip(EquipableItem equipable)
|
|
{
|
|
HealthComponent.SetMaximumHealth(HealthComponent.MaximumHP.Value - equipable.BonusHP);
|
|
VTComponent.SetMaximumVT(VTComponent.MaximumVT.Value - equipable.BonusVT);
|
|
|
|
EquipmentComponent.Unequip(equipable);
|
|
}
|
|
|
|
private static Vector3 GlobalInputVector
|
|
{
|
|
get
|
|
{
|
|
var rawInput = Input.GetVector(GameInputs.MoveLeft, GameInputs.MoveRight, GameInputs.MoveUp, GameInputs.MoveDown);
|
|
var input = new Vector3
|
|
{
|
|
X = rawInput.X,
|
|
Z = rawInput.Y
|
|
};
|
|
return input with { Y = 0f };
|
|
}
|
|
}
|
|
|
|
private static float LeftStrafeInputVector => Input.GetActionStrength(GameInputs.StrafeLeft);
|
|
|
|
private static float RightStrafeInputVector => Input.GetActionStrength(GameInputs.StrafeRight);
|
|
|
|
private void Attack()
|
|
{
|
|
if (PlayerIsHittingGeometry())
|
|
AnimationPlayer.Play("hit_wall");
|
|
else if (!AnimationPlayer.IsPlaying())
|
|
PlayAttackAnimation();
|
|
}
|
|
|
|
private void ThrowItem()
|
|
{
|
|
var itemScene = GD.Load<PackedScene>("res://src/items/throwable/ThrowableItem.tscn");
|
|
var throwItem = itemScene.Instantiate<ThrowableItem>();
|
|
GetTree().Root.AddChildEx(throwItem);
|
|
throwItem.GlobalPosition = CurrentPosition + new Vector3(0, 3.5f, 0);
|
|
throwItem.GlobalRotation = GlobalRotation;
|
|
}
|
|
|
|
private void PlayAttackAnimation()
|
|
{
|
|
SfxDatabase.Instance.Play(((Weapon)EquipmentComponent.EquippedWeapon.Value).SoundEffect);
|
|
var attackSpeed = ((Weapon)EquipmentComponent.EquippedWeapon.Value).AttackSpeed;
|
|
AnimationPlayer.SetSpeedScale((float)attackSpeed);
|
|
AnimationPlayer.Play("attack");
|
|
}
|
|
|
|
private void OnExitTree()
|
|
{
|
|
PlayerLogic.Stop();
|
|
PlayerBinding.Dispose();
|
|
Hitbox.AreaEntered -= Hitbox_AreaEntered;
|
|
CollisionDetector.AreaEntered -= CollisionDetector_AreaEntered;
|
|
HealthComponent.HealthReachedZero -= Die;
|
|
HealthTimer.Timeout -= OnHealthTimerTimeout;
|
|
}
|
|
|
|
private void Move(float delta)
|
|
{
|
|
var rawInput = GlobalInputVector;
|
|
var strafeLeftInput = LeftStrafeInputVector;
|
|
var strafeRightInput = RightStrafeInputVector;
|
|
|
|
var transform = Transform;
|
|
transform.Basis = new Basis(Vector3.Up, Settings.RotationSpeed * -rawInput.X * delta) * transform.Basis;
|
|
var moveDirection = new Vector3(strafeRightInput - strafeLeftInput, 0, rawInput.Z).Normalized();
|
|
var velocity = Basis * moveDirection * Settings.MoveSpeed * Settings.Acceleration;
|
|
_knockbackStrength *= 0.9f;
|
|
Transform = Transform with { Basis = transform.Basis };
|
|
Velocity = velocity + (_knockbackDirection * _knockbackStrength);
|
|
if (!WalkSFX.Playing && !Velocity.IsZeroApprox())
|
|
WalkSFX.Play();
|
|
else if (Velocity.IsZeroApprox())
|
|
WalkSFX.Stop();
|
|
MoveAndSlide();
|
|
}
|
|
|
|
private void OnPlayerPositionUpdated(Vector3 globalPosition) => GlobalPosition = globalPosition;
|
|
|
|
private void OnHealthTimerTimeout()
|
|
{
|
|
if (VTComponent.CurrentVT.Value > 0)
|
|
{
|
|
if (((Accessory)EquipmentComponent.EquippedAccessory.Value).AccessoryTag == AccessoryTag.HalfVTConsumption)
|
|
reduceOnTick = !reduceOnTick;
|
|
|
|
HealthComponent.Heal(1);
|
|
|
|
if (reduceOnTick)
|
|
VTComponent.Reduce(1);
|
|
}
|
|
else
|
|
HealthComponent.Damage(1);
|
|
}
|
|
|
|
private void Hitbox_AreaEntered(Area3D area)
|
|
{
|
|
var target = area.GetOwner();
|
|
if (target is IEnemy enemy)
|
|
HitEnemy(enemy);
|
|
}
|
|
|
|
private void HitEnemy(IEnemy enemy)
|
|
{
|
|
var ignoreElementalResistance = (EquipmentComponent.EquippedWeapon.Value as Weapon).WeaponTag == WeaponTag.IgnoreAffinity;
|
|
var ignoreDefense = (EquipmentComponent.EquippedWeapon.Value as Weapon).WeaponTag == WeaponTag.IgnoreDefense;
|
|
var isCriticalHit = BattleExtensions.IsCriticalHit(LuckComponent.Luck.Value + EquipmentComponent.BonusLuck);
|
|
var totalDamage = AttackComponent.CurrentAttack.Value + EquipmentComponent.BonusAttack;
|
|
var element = (EquipmentComponent.EquippedWeapon.Value as Weapon).WeaponElement;
|
|
|
|
if (isCriticalHit)
|
|
{
|
|
totalDamage += (int)(totalDamage * 0.5f);
|
|
SfxDatabase.Instance.Play(SoundEffect.Crit);
|
|
}
|
|
|
|
var baseAttack = new AttackData(totalDamage, element, ignoreDefense, ignoreElementalResistance);
|
|
var damageDealt = DamageCalculator.CalculateDamage(baseAttack, enemy.DefenseComponent.CurrentDefense.Value, ElementalResistanceSet.None);
|
|
enemy.HealthComponent.Damage(damageDealt);
|
|
|
|
if (((Weapon)EquipmentComponent.EquippedWeapon.Value).WeaponTag == WeaponTag.Knockback && enemy is IKnockbackable knockbackable)
|
|
knockbackable.Knockback(0.3f, -CurrentBasis.Z.Normalized());
|
|
if (((Weapon)EquipmentComponent.EquippedWeapon.Value).WeaponTag == WeaponTag.SelfDamage)
|
|
HealthComponent.Damage(5);
|
|
}
|
|
|
|
private void CollisionDetector_AreaEntered(Area3D area)
|
|
{
|
|
if (area.GetParent() is InventoryItem inventoryItem)
|
|
{
|
|
var isAdded = Inventory.PickUpItem(inventoryItem);
|
|
if (isAdded)
|
|
inventoryItem.QueueFree();
|
|
}
|
|
if (area.GetParent() is DroppedItem droppedItem)
|
|
{
|
|
var isAdded = Inventory.PickUpItem(droppedItem.Item);
|
|
if (isAdded)
|
|
droppedItem.QueueFree();
|
|
}
|
|
if (area.GetParent() is ThrownItem thrownItem)
|
|
{
|
|
var isAdded = Inventory.PickUpItem(thrownItem.ItemThatIsThrown);
|
|
if (isAdded)
|
|
thrownItem.QueueFree();
|
|
}
|
|
if (area.GetParent() is Restorative restorative)
|
|
{
|
|
restorative.QueueFree();
|
|
}
|
|
}
|
|
|
|
private bool PlayerIsHittingGeometry()
|
|
{
|
|
var collisions = WallCheck.GetCollidingBodies();
|
|
return collisions.Count > 0;
|
|
}
|
|
|
|
private void WallCheck_BodyEntered(Node body)
|
|
{
|
|
GD.Print("Hit wall");
|
|
AnimationPlayer.Stop();
|
|
}
|
|
}
|