using Chickensoft.AutoInject; using Chickensoft.GodotNodeInterfaces; using Chickensoft.Introspection; using Godot; using SimpleInjector; using System; using System.Linq; 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, 60, 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 WeaponAnimations { get; set; } = default!; [Node] private IAnimationPlayer PlayerFXAnimations { get; set; } = default!; [Node] private IAnimationPlayer TakeDamageAnimationPlayer { 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!; [Node] private CollisionShape3D MainCollision { get; set; } = default!; [Node] private ShakeCamera _camera3D { get; set; } = default!; [Node] private Projectile FireReactor { get; set; } = default!; [Node] private Projectile AirReactor { get; set; } = default!; [Node] private Projectile WaterReactor { get; set; } = default!; [Node] private Projectile PersuaderBullet { get; set; } = default!; [Node] private Sprite2D PersuaderCrosshair { get; set; } = default!; #endregion [Export] public int HealthTimerHPRate { get; set; } = 2; [Export] public bool AutoIdentifyItems { get; set; } = false; [Export] public float HealthTimerSpeedModifier { get; set; } = 1f; [Export] public bool AutoRevive { get; set; } = false; private bool flipAttack = false; private float _healthTimerWaitTime = 3.0f; private bool _healthTimerActive = true; private bool _debugSprint = false; private float _knockbackStrength = 0.0f; private Vector3 _knockbackDirection = Vector3.Zero; private ItemReroller _itemReroller; private PlayerEffectService _playerEffectService; 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(); Inventory.InventoryChanged += Inventory_InventoryChanged; 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); _playerEffectService = new PlayerEffectService(this); 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() { PlayerFXAnimations.Play("RESET"); Inventory.Items.Clear(); 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.CurrentHP.Changed += InverseHPToAttackPowerSync; HealthComponent.HealthReachedZero += Die; ExperiencePointsComponent.PlayerLevelUp += OnLevelUp; ExperiencePointsComponent.PlayerLevelDown += OnLevelDown; PlayerFXAnimations.AnimationFinished += PlayerFXAnimations_AnimationFinished; HealthTimer.WaitTime = _healthTimerWaitTime; SetProcessInput(false); SetPhysicsProcess(false); } #endregion public void Activate() { SetProcessInput(true); SetPhysicsProcess(true); SetHealthTimerStatus(HealthTimerIsActive); } public void Deactivate() { Velocity = Vector3.Zero; SetProcessInput(false); SetPhysicsProcess(false); SetHealthTimerStatus(false); } public void SetHealthTimerStatus(bool isActive) { if (isActive) HealthTimer.Start(); else HealthTimer.Stop(); } public void ModifyHealthTimerSpeed(float newSpeed) { HealthTimerSpeedModifier = newSpeed; HealthTimer.Stop(); HealthTimer.WaitTime = _healthTimerWaitTime * newSpeed; HealthTimer.Start(); } public void ModifyHealthRecoveryAmount(int newAmount) { } public void TeleportPlayer((Vector3 Rotation, Vector3 Position) newTransform) { Rotation = newTransform.Rotation; Position = newTransform.Position; ResetPhysicsInterpolation(); } public void TakeDamage(AttackData damage) { _camera3D.AddShake(1.0f); TakeDamageAnimationPlayer.Play("take_damage"); var damageReceived = DamageCalculator.CalculateDamage(damage, TotalDefense, EquipmentComponent.ElementalResistance); HealthComponent.Damage(damageReceived); SfxDatabase.Instance.Play(SoundEffect.TakeDamage); } public void Knockback(float impulse) { _knockbackStrength = impulse; _knockbackDirection = GlobalBasis.Z.Normalized(); } public void PlayJumpScareAnimation() => PlayerFXAnimations.Play("jump_scare"); public void IdentifyItem(IBaseInventoryItem unidentifiedItem) => _itemReroller.RerollItem(unidentifiedItem, Inventory); public int TotalAttack => AttackComponent.CurrentAttack.Value + EquipmentComponent.BonusAttack; public int TotalDefense => DefenseComponent.CurrentDefense.Value + EquipmentComponent.BonusDefense; public int TotalLuck => LuckComponent.Luck.Value + EquipmentComponent.BonusLuck; public void LevelUp() { ExperiencePointsComponent.LevelUp(); } public void Die() { PlayerFXAnimations.Play("death"); if (AutoRevive) return; HealthTimer.WaitTime = _healthTimerWaitTime; HealthTimer.Timeout -= OnHealthTimerTimeout; SetProcessInput(false); SetPhysicsProcess(false); } public override void _Input(InputEvent @event) { if (@event.IsActionPressed(GameInputs.Attack)) Attack(); if (@event.IsActionPressed(GameInputs.Sprint)) _debugSprint = true; else if (@event.IsActionReleased(GameInputs.Sprint)) _debugSprint = false; } public void OnPhysicsProcess(double delta) { PlayerLogic.Input(new PlayerLogic.Input.PhysicsTick(delta)); } public void Equip(IEquipableItem 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); if (equipable is IAugmentableItem augmentable) if (augmentable.Augment != null) augmentable.Augment.AugmentType.Apply(); EquipmentComponent.Equip(equipable); SfxDatabase.Instance.Play(SoundEffect.Equip); if (equipable is Weapon weapon && weapon.WeaponTag == WeaponTag.KineticProjectile) PersuaderCrosshair.Show(); } public void Unequip(IEquipableItem equipable) { HealthComponent.SetMaximumHealth(HealthComponent.MaximumHP.Value - equipable.BonusHP); VTComponent.SetMaximumVT(VTComponent.MaximumVT.Value - equipable.BonusVT); if (equipable is IAugmentableItem augmentItem && augmentItem.Augment != null) augmentItem.Augment.AugmentType.Remove(); EquipmentComponent.Unequip(equipable); SfxDatabase.Instance.Play(SoundEffect.Unequip); if (equipable is Weapon weapon && weapon.WeaponTag == WeaponTag.KineticProjectile) PersuaderCrosshair.Hide(); } public void ApplyNewAugment(IAugmentItem jewel, IAugmentableItem augmentableItem) { Inventory.Remove(jewel); ApplyNewAugment((dynamic)augmentableItem, (jewel as Jewel).Stats.JewelTag); if (augmentableItem is IEquipableItem equipable && EquipmentComponent.IsItemEquipped(equipable)) augmentableItem.Augment.AugmentType.Apply(); } private void ApplyNewAugment(Weapon weapon, JewelTags tag) { switch (tag) { case JewelTags.AeolicElement: weapon.Stats.WeaponElement = ElementType.Aeolic; weapon.Augment = new Augment(JewelTags.AeolicElement, new BasicAugment()); break; case JewelTags.HydricElement: weapon.Stats.WeaponElement = ElementType.Hydric; weapon.Augment = new Augment(JewelTags.HydricElement, new BasicAugment()); break; case JewelTags.SlowVTReduction: weapon.Augment = new Augment(JewelTags.SlowVTReduction, new SlowVTReductionAugment(this)); break; case JewelTags.HastenVT: weapon.Augment = new Augment(JewelTags.HastenVT, new HastenVTAugment(this)); break; case JewelTags.ReviveUserOnce: weapon.Augment = new Augment(JewelTags.ReviveUserOnce, new RevivePlayerAugment(this)); break; case JewelTags.IncreaseHPRecovery: weapon.Augment = new Augment(JewelTags.IncreaseHPRecovery, new HPRecoverySpeedAugment(this)); break; case JewelTags.LowerEXPGain: weapon.Augment = new Augment(JewelTags.LowerEXPGain, new LowerEXPRateAugment(this)); break; case JewelTags.ItemRescue: Inventory.Remove(weapon); break; case JewelTags.Glue: weapon.Glued = true; weapon.Augment = new Augment(JewelTags.Glue, new BasicAugment()); break; case JewelTags.TelluricElement: weapon.Stats.WeaponElement = ElementType.Telluric; weapon.Augment = new Augment(JewelTags.TelluricElement, new BasicAugment()); break; case JewelTags.AutoIdentifyAllItems: weapon.Augment = new Augment(JewelTags.AutoIdentifyAllItems, new IdentifyAllItemsAugment(this)); break; case JewelTags.IncreaseAtkDefLuck: weapon.IncreaseAttack(2); weapon.IncreaseDefense(2); weapon.IncreaseLuck(10); weapon.Augment = new Augment(JewelTags.IncreaseAtkDefLuck, new BasicAugment()); break; case JewelTags.IncreaseLuck: weapon.IncreaseLuck(25); weapon.Augment = new Augment(JewelTags.IncreaseLuck, new BasicAugment()); break; } } private void ApplyNewAugment(Armor armor, JewelTags tag) { switch (tag) { case JewelTags.AeolicElement: armor.Stats.AeolicResistance += 25; armor.Augment = new Augment(JewelTags.AeolicElement, new BasicAugment()); break; case JewelTags.HydricElement: armor.Stats.HydricResistance += 25; armor.Augment = new Augment(JewelTags.HydricElement, new BasicAugment()); break; case JewelTags.SlowVTReduction: armor.Augment = new Augment(JewelTags.SlowVTReduction, new SlowVTReductionAugment(this)); break; case JewelTags.HastenVT: armor.Augment = new Augment(JewelTags.HastenVT, new HastenVTAugment(this)); break; case JewelTags.ReviveUserOnce: armor.Augment = new Augment(JewelTags.ReviveUserOnce, new RevivePlayerAugment(this)); break; case JewelTags.IncreaseHPRecovery: armor.Augment = new Augment(JewelTags.IncreaseHPRecovery, new HPRecoverySpeedAugment(this)); break; case JewelTags.LowerEXPGain: armor.Augment = new Augment(JewelTags.LowerEXPGain, new LowerEXPRateAugment(this)); break; case JewelTags.ItemRescue: Inventory.Remove(armor); break; case JewelTags.Glue: armor.Glued = true; armor.Augment = new Augment(JewelTags.Glue, new BasicAugment()); break; case JewelTags.TelluricElement: armor.Stats.TelluricResistance += 25; armor.Augment = new Augment(JewelTags.TelluricElement, new BasicAugment()); break; case JewelTags.AutoIdentifyAllItems: armor.Augment = new Augment(JewelTags.AutoIdentifyAllItems, new IdentifyAllItemsAugment(this)); break; case JewelTags.IncreaseAtkDefLuck: armor.IncreaseAttack(2); armor.IncreaseDefense(2); armor.IncreaseLuck(10); armor.Augment = new Augment(JewelTags.IncreaseAtkDefLuck, new BasicAugment()); break; case JewelTags.IncreaseLuck: armor.IncreaseLuck(25); armor.Augment = new Augment(JewelTags.IncreaseLuck, new BasicAugment()); break; } } private void ApplyNewAugment(Accessory accessory, JewelTags tag) { switch (tag) { case JewelTags.AeolicElement: accessory.Stats.AeolicResistance += 25; accessory.Augment = new Augment(JewelTags.AeolicElement, new BasicAugment()); break; case JewelTags.HydricElement: accessory.Stats.HydricResistance += 25; accessory.Augment = new Augment(JewelTags.HydricElement, new BasicAugment()); break; case JewelTags.SlowVTReduction: accessory.Augment = new Augment(JewelTags.SlowVTReduction, new SlowVTReductionAugment(this)); break; case JewelTags.HastenVT: accessory.Augment = new Augment(JewelTags.HastenVT, new HastenVTAugment(this)); break; case JewelTags.ReviveUserOnce: accessory.Augment = new Augment(JewelTags.ReviveUserOnce, new RevivePlayerAugment(this)); break; case JewelTags.IncreaseHPRecovery: accessory.Augment = new Augment(JewelTags.IncreaseHPRecovery, new HPRecoverySpeedAugment(this)); break; case JewelTags.LowerEXPGain: accessory.Augment = new Augment(JewelTags.LowerEXPGain, new LowerEXPRateAugment(this)); break; case JewelTags.ItemRescue: Inventory.Remove(accessory); break; case JewelTags.Glue: accessory.Glued = true; accessory.Augment = new Augment(JewelTags.Glue, new BasicAugment()); break; case JewelTags.TelluricElement: accessory.Stats.TelluricResistance += 25; accessory.Augment = new Augment(JewelTags.TelluricElement, new BasicAugment()); break; case JewelTags.AutoIdentifyAllItems: accessory.Augment = new Augment(JewelTags.AutoIdentifyAllItems, new IdentifyAllItemsAugment(this)); break; case JewelTags.IncreaseAtkDefLuck: accessory.IncreaseAttack(2); accessory.IncreaseDefense(2); accessory.IncreaseLuck(10); accessory.Augment = new Augment(JewelTags.IncreaseAtkDefLuck, new BasicAugment()); break; case JewelTags.IncreaseLuck: accessory.IncreaseLuck(25); accessory.Augment = new Augment(JewelTags.IncreaseLuck, new BasicAugment()); break; } } 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 void OnLevelUp() { BoostPlayerHPFromLevelUp(); } private void OnLevelDown() { LowerPlayerHPFromLevelDown(); } private void BoostPlayerHPFromLevelUp() { var rng = new RandomNumberGenerator(); rng.Randomize(); var hpIncrease = rng.RandiRange(3, 6); HealthComponent.RaiseMaximumHP(hpIncrease); } private void LowerPlayerHPFromLevelDown() { var rng = new RandomNumberGenerator(); rng.Randomize(); var hpIncrease = rng.RandiRange(3, 6); HealthComponent.LowerMaximumHP(hpIncrease); } private static float LeftStrafeInputVector => Input.GetActionStrength(GameInputs.StrafeLeft); private static float RightStrafeInputVector => Input.GetActionStrength(GameInputs.StrafeRight); private void Attack() { var weapon = EquipmentComponent.EquippedWeapon.Value as Weapon; if (weapon.WeaponTag == WeaponTag.ElementalProjectile || weapon.WeaponTag == WeaponTag.KineticProjectile) { HandleProjectile(weapon); return; } if (PlayerIsHittingGeometry()) WeaponAnimations.Play("hit_wall"); else if (!WeaponAnimations.IsPlaying()) PlayAttackAnimation(); else return; if (weapon.WeaponTag == WeaponTag.DegradeOnSwing) _playerEffectService.Degrade(); else if (weapon.WeaponTag == WeaponTag.SelfDamage) _playerEffectService.TakeSelfDamage(5); } private void HandleProjectile(Weapon weapon) { var ammo = EquipmentComponent.EquippedAmmo.Value as Ammo; if (ammo.Count == null || ammo.Count?.Value <= 0) return; var fired = false; if (weapon.WeaponTag == WeaponTag.ElementalProjectile) { if (ammo.AmmoElement == ElementType.Igneous) fired = FireReactor.Fire(); if (ammo.AmmoElement == ElementType.Aeolic) fired = AirReactor.Fire(); if (ammo.AmmoElement == ElementType.Hydric) fired = WaterReactor.Fire(); } if (weapon.WeaponTag == WeaponTag.KineticProjectile) fired = PersuaderBullet.Fire(); if (!fired) return; ammo.SetCount(ammo.Count.Value - 1); EquipmentComponent.UpdateEquipment(ammo); if (ammo.Count.Value <= 0) { EquipmentComponent.Unequip(ammo); Inventory.Remove(ammo); } } private void ThrowItem() { var itemScene = GD.Load("res://src/items/throwable/ThrowableItem.tscn"); var throwItem = itemScene.Instantiate(); GetTree().Root.AddChildEx(throwItem); throwItem.GlobalPosition = CurrentPosition; throwItem.GlobalRotation = GlobalRotation; } private void PlayAttackAnimation() { var weapon = (Weapon)EquipmentComponent.EquippedWeapon.Value; SfxDatabase.Instance.Play(weapon.SoundEffect); WeaponAnimations.SetSpeedScale((float)weapon.AttackSpeed); var potentialAnimName = weapon.Stats.Name; if (WeaponAnimations.HasAnimation(potentialAnimName)) WeaponAnimations.Play(potentialAnimName); else if (weapon.WeaponElement == ElementType.Aeolic) WeaponAnimations.Play("Air Slash"); else if (weapon.WeaponElement == ElementType.Hydric) WeaponAnimations.Play("Water Slash"); else if (weapon.WeaponElement == ElementType.Igneous) WeaponAnimations.Play("Fire Slash"); else if (weapon.WeaponElement == ElementType.Telluric) WeaponAnimations.Play("Earth Slash"); else if (string.IsNullOrWhiteSpace(potentialAnimName)) WeaponAnimations.Play("Unarmed"); else WeaponAnimations.Play("Normal Slash"); } private void PlayerFXAnimations_AnimationFinished(StringName animName) { if (animName == "death") { if (AutoRevive) PlayerFXAnimations.PlayBackwards("revive"); else PlayerDied?.Invoke(); } if (animName == "revive") { Revive(); } } private void Revive() { HealthComponent.SetCurrentHealth(HealthComponent.MaximumHP.Value); VTComponent.SetVT(VTComponent.MaximumVT.Value); if (EquipmentComponent.EquippedAccessory.Value.Augment?.AugmentTag == JewelTags.ReviveUserOnce) { var itemToBreak = EquipmentComponent.EquippedAccessory.Value; Unequip(EquipmentComponent.EquippedAccessory.Value); Inventory.Remove(itemToBreak); } else if (EquipmentComponent.EquippedArmor.Value.Augment?.AugmentTag == JewelTags.ReviveUserOnce) { var itemToBreak = EquipmentComponent.EquippedArmor.Value; Unequip(EquipmentComponent.EquippedArmor.Value); Inventory.Remove(itemToBreak); } else if (EquipmentComponent.EquippedWeapon.Value.Augment?.AugmentTag == JewelTags.ReviveUserOnce) { var itemToBreak = EquipmentComponent.EquippedWeapon.Value; Unequip(EquipmentComponent.EquippedWeapon.Value); Inventory.Remove(itemToBreak); } else PlayJumpScareAnimation(); } private void InverseHPToAttackPowerSync(int obj) { var weapon = (Weapon)EquipmentComponent.EquippedWeapon.Value; if (weapon.WeaponTag == WeaponTag.InverseHPAttackPower) { var healthPercentage = (HealthComponent.CurrentHP.Value * 10) / HealthComponent.MaximumHP.Value; weapon.SetAttack(10 - healthPercentage); EquipmentComponent.Equip(weapon); } } private void OnExitTree() { PlayerLogic.Stop(); PlayerBinding.Dispose(); Hitbox.AreaEntered -= Hitbox_AreaEntered; CollisionDetector.AreaEntered -= CollisionDetector_AreaEntered; HealthTimer.Timeout -= OnHealthTimerTimeout; HealthComponent.CurrentHP.Changed -= InverseHPToAttackPowerSync; HealthComponent.HealthReachedZero -= Die; ExperiencePointsComponent.PlayerLevelUp -= OnLevelUp; PlayerFXAnimations.AnimationFinished -= PlayerFXAnimations_AnimationFinished; } 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(); if (moveDirection.Length() > 0.25f) { var rng = new RandomNumberGenerator(); rng.Randomize(); WalkSFX.PitchScale = rng.RandfRange(0.5f, 1.5f); if (!WalkSFX.Playing) WalkSFX.Play(); } else if (WalkSFX.Playing) WalkSFX.Stop(); var velocity = (Basis * moveDirection * Settings.MoveSpeed * Settings.Acceleration); if (_debugSprint) velocity *= 2; _knockbackStrength *= 0.9f; Transform = Transform with { Basis = transform.Basis }; Velocity = velocity + (_knockbackDirection * _knockbackStrength); MoveAndSlide(); } private void OnPlayerPositionUpdated(Vector3 globalPosition) => GlobalPosition = globalPosition; private void OnHealthTimerTimeout() { if (VTComponent.CurrentVT.Value > 0) { if (((Accessory)EquipmentComponent.EquippedAccessory.Value).AccessoryTag == AccessoryTag.HalfVTConsumption) _healthTimerActive = !_healthTimerActive; HealthComponent.Heal(1); if (_healthTimerActive) 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 weapon = EquipmentComponent.EquippedWeapon.Value as Weapon; var isCriticalHit = BattleExtensions.IsCriticalHit(TotalLuck); var totalDamage = TotalAttack; if (isCriticalHit) { totalDamage += (int)(totalDamage * 0.5f); SfxDatabase.Instance.Play(SoundEffect.Crit); } var baseAttack = new AttackData(totalDamage, weapon.WeaponElement, weapon.WeaponTag == WeaponTag.IgnoreDefense, weapon.WeaponTag == WeaponTag.IgnoreAffinity); var damageDealt = DamageCalculator.CalculateDamage(baseAttack, enemy.DefenseComponent.CurrentDefense.Value, enemy.ElementalResistanceSet); enemy.HealthComponent.Damage(damageDealt); if (weapon.WeaponTag == WeaponTag.Knockback && enemy is IKnockbackable knockbackable) knockbackable.Knockback(0.3f, -CurrentBasis.Z.Normalized()); if (weapon.WeaponTag == WeaponTag.SelfDamage) _playerEffectService.TakeSelfDamage(weapon.Stats.SelfDamage); if (weapon.WeaponTag == WeaponTag.Instakill) _playerEffectService.Instakill(enemy); } private async void CollisionDetector_AreaEntered(Area3D area) { if (area.GetParent() is IBaseInventoryItem inventoryItem) { var isAdded = Inventory.PickUpItem(inventoryItem); if (isAdded) ((Node3D)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) { await ToSignal(GetTree().CreateTimer(0.2f), "timeout"); VTComponent.Restore(restorative.RestoreAmount); SfxDatabase.Instance.Play(SoundEffect.HealVT); restorative.QueueFree(); } } private bool PlayerIsHittingGeometry() { var collisions = WallCheck.GetCollidingBodies(); return collisions.Count > 0; } private void WallCheck_BodyEntered(Node body) { GD.Print("Hit wall"); WeaponAnimations.Stop(); } private void Inventory_InventoryChanged() { if (AutoIdentifyItems) { foreach (var item in Inventory.Items.ToList()) { if (item.ItemTag == ItemTag.MysteryItem) IdentifyItem(item); } } } }