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 { #region Dependency Injection public override void _Notification(int what) => this.Notify(what); IPlayer IProvide.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(Lifestyle.Singleton); PlayerLogic = container.GetInstance(); 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("res://src/items/throwable/ThrowableItem.tscn"); var throwItem = itemScene.Instantiate(); 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(); } }