using Chickensoft.AutoInject; using Chickensoft.Collections; using Chickensoft.GodotNodeInterfaces; using Chickensoft.Introspection; using Chickensoft.SaveFileBuilder; using Godot; using Godot.Collections; using SimpleInjector; using System; using Zennysoft.Ma.Adapter; 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); private PlayerLogic.IBinding PlayerBinding { get; set; } = default!; [Dependency] private IGameRepo _gameRepo => this.DependOn(() => new GameRepo()); #endregion #region Save public ISaveChunk PlayerChunk { get; set; } = default!; #endregion public double CurrentHP => Stats.CurrentHP.Value; public Vector3 CurrentPosition => GlobalPosition; public Basis CurrentBasis => Transform.Basis; public PlayerStats Stats { get; set; } = default!; public IInventory Inventory { get; private set; } = default!; public AutoProp EquippedWeapon => _equippedWeapon; private AutoProp _equippedWeapon { get; set; } = new AutoProp(new Weapon()); public AutoProp EquippedArmor => _equippedArmor; private AutoProp _equippedArmor { get; set; } = new AutoProp(new Armor()); public AutoProp EquippedAccessory => _equippedAccessory; private AutoProp _equippedAccessory { get; set; } = new AutoProp(new Accessory()); private PlayerLogic.Settings Settings { get; set; } = default!; private IPlayerLogic PlayerLogic { get; set; } = default!; #region Dependencies [Dependency] public IGame Game => this.DependOn(); [Dependency] public ISaveChunk GameChunk => this.DependOn>(); ISaveChunk IProvide>.Value() => PlayerChunk; #endregion #region Exports [Export] private PlayerStatResource _playerStatResource { get; set; } = default!; #endregion #region Node Dependencies [Node] private IAnimationPlayer AnimationPlayer { get; set; } = default!; [Node] private AnimatedSprite2D SwordSlashAnimation { get; set; } = default!; [Node] private IHitbox 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!; #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 Dictionary _expToNextLevel; private DamageCalculator _damageCalculator; #region Initialization public void InitializePlayerState() { Inventory = new Inventory(); Stats = InitializePlayerStats(); SetProcessInput(false); SetPhysicsProcess(false); EquippedWeapon.Changed += EquippedWeapon_Sync; EquippedArmor.Changed += EquippedArmor_Sync; EquippedAccessory.Changed += EquippedAccessory_Sync; Stats.CurrentHP.Changed += CurrentHP_Sync; Stats.CurrentExp.Changed += CurrentEXP_Sync; HealthTimer.WaitTime = _healthTimerWaitTime; HealthTimer.Timeout += OnHealthTimerTimeout; } public void Setup() { var container = new SimpleInjector.Container(); container.Register(Lifestyle.Singleton); //container.Verify(); PlayerLogic = container.GetInstance(); PlayerLogic.Set(this as IPlayer); PlayerLogic.Set(Settings); PlayerLogic.Set(Stats); PlayerLogic.Set(_gameRepo); _expToNextLevel = new Dictionary { { 2, 12 }, { 3, 39 }, { 4, 87 }, { 5, 162 }, { 6, 270 }, { 7, 417 }, { 8, 609 } }; _damageCalculator = new DamageCalculator(); Hitbox.AreaEntered += Hitbox_AreaEntered; CollisionDetector.AreaEntered += CollisionDetector_AreaEntered; AnimationPlayer.AnimationFinished += OnAnimationFinished; } public void OnResolved() { Settings = new PlayerLogic.Settings() { RotationSpeed = _playerStatResource.RotationSpeed, MoveSpeed = _playerStatResource.MoveSpeed, Acceleration = _playerStatResource.Acceleration }; PlayerChunk = new SaveChunk( onSave: (chunk) => new PlayerData() { PlayerStats = Stats, Inventory = Inventory }, onLoad: (chunk, data) => { Stats = new PlayerStats( data.PlayerStats.CurrentHP, data.PlayerStats.MaximumHP, data.PlayerStats.CurrentVT, data.PlayerStats.MaximumVT, data.PlayerStats.CurrentAttack, data.PlayerStats.BonusAttack, data.PlayerStats.MaxAttack, data.PlayerStats.CurrentDefense, data.PlayerStats.BonusDefense, data.PlayerStats.MaxDefense, data.PlayerStats.CurrentExp, data.PlayerStats.CurrentLevel, data.PlayerStats.ExpToNextLevel, data.PlayerStats.Luck); Inventory = data.Inventory; } ); PlayerBinding = PlayerLogic.Bind(); PlayerBinding .Handle((in PlayerLogic.Output.Animations.Attack output) => { if (PlayerIsHittingGeometry()) { AnimationPlayer.Play("hit_wall"); _gameRepo.OnPlayerAttackedWall(); } else { PlayAttackAnimation(); } }) .Handle((in PlayerLogic.Output.ThrowItem output) => { }) .Handle((in PlayerLogic.Output.Move output) => { Move(output.delta); }); GameChunk.AddChunk(PlayerChunk); PlayerLogic.Start(); this.Provide(); SetProcessInput(false); SetPhysicsProcess(false); } public void OnReady() { SwordSlashAnimation.Position = GetViewport().GetVisibleRect().Size / 2; } #endregion public void Activate() { SetProcessInput(true); SetPhysicsProcess(true); HealthTimer.Start(); } public void Attack() { PlayerLogic.Input(new PlayerLogic.Input.Attack()); } public void RaiseHP(int amountToRaise) { Stats.SetMaximumHP(Stats.MaximumHP.Value + amountToRaise); Stats.SetCurrentHP(Stats.MaximumHP.Value); _gameRepo.AnnounceMessageInInventory($"{amountToRaise}MAXHP Up."); } public void HealHP(int amountToRestore) { Stats.SetCurrentHP(Stats.CurrentHP.Value + amountToRestore); var raiseString = amountToRestore == 1000 ? "MAX" : $"{amountToRestore}"; _gameRepo.AnnounceMessageInInventory($"{raiseString}HP Restored."); } public void RaiseVT(int amountToRaise) { if (Stats.CurrentVT == Stats.MaximumVT) { Stats.SetMaximumVT(Stats.MaximumVT.Value + amountToRaise); Stats.SetCurrentVT(Stats.MaximumVT.Value); _gameRepo.AnnounceMessageInInventory($"{amountToRaise}MAXVT Up."); } } public void HealVT(int amountToRestore) { Stats.SetCurrentVT(Stats.CurrentVT.Value + amountToRestore); var raiseString = amountToRestore == 1000 ? "MAX" : $"{amountToRestore}"; _gameRepo.AnnounceMessageInInventory($"{raiseString}VT Restored."); } public void ModifyBonusAttack(int amount) { Stats.SetBonusAttack(Stats.BonusAttack.Value + amount); } public void ModifyBonusDefense(int amount) { Stats.SetBonusDefense(Stats.BonusDefense.Value + amount); } public void ModifyMaximumHP(int amount) { Stats.SetMaximumHP(Stats.MaximumHP.Value + amount); } public void ModifyMaximumVT(int amount) { Stats.SetMaximumVT(Stats.MaximumVT.Value + amount); } public void ModifyBonusLuck(double amount) { Stats.SetLuck(Stats.Luck.Value + amount); } public 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); MoveAndSlide(); } public void TeleportPlayer(Transform3D newTransform) { Transform = newTransform; } public void TakeDamage(double damage, ElementType elementType, bool isCriticalHit = false) { if (Stats.CurrentHP.Value > 0) { _damageCalculator.CalculateDamage(damage, elementType, Stats.CurrentDefense.Value + Stats.BonusDefense.Value, ((Armor)_equippedArmor.Value).Stats.ElementalResistanceSet); Stats.SetCurrentHP(Stats.CurrentHP.Value - (int)damage); } } public void Knockback(float impulse) { _knockbackStrength = impulse; _knockbackDirection = GlobalBasis.Z.Normalized(); } public void GainExp(double expGained) { Stats.SetCurrentExp(Stats.CurrentExp.Value + expGained); } public void LevelUp() { var rng = new RandomNumberGenerator(); rng.Randomize(); var hpIncrease = rng.RandiRange(3, 6); Stats.SetMaximumHP(Stats.MaximumHP.Value + hpIncrease); var nextLevel = Stats.CurrentLevel.Value + 1; var expToNextLevel = _expToNextLevel[nextLevel]; var newCurrentExp = Mathf.Max(Stats.CurrentExp.Value - Stats.ExpToNextLevel.Value, 0); Stats.SetCurrentLevel(nextLevel); Stats.SetExpToNextLevel(expToNextLevel); Stats.SetCurrentExp(newCurrentExp); } public void Die() { EquippedWeapon.Sync -= EquippedWeapon_Sync; EquippedArmor.Sync -= EquippedArmor_Sync; EquippedAccessory.Sync -= EquippedAccessory_Sync; Stats.CurrentHP.Sync -= CurrentHP_Sync; Stats.CurrentExp.Sync -= CurrentEXP_Sync; HealthTimer.WaitTime = _healthTimerWaitTime; HealthTimer.Timeout -= OnHealthTimerTimeout; SetProcessInput(false); SetPhysicsProcess(false); //Hitbox.AreaEntered -= Hitbox_AreaEntered; //CollisionDetector.AreaEntered -= CollisionDetector_AreaEntered; //AnimationPlayer.AnimationFinished -= OnAnimationFinished; Game.GameOver(); } public override void _Input(InputEvent @event) { if (@event.IsActionPressed(GameInputs.Attack)) Attack(); } 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 = Game.RerollItem(equipable) as EquipableItem; Equip(rerolledItem); return; } if (equipable is Weapon weapon) { Unequip(_equippedWeapon.Value); weapon.IsEquipped = true; _equippedWeapon.OnNext(weapon); } else if (equipable is Armor armor) { Unequip(_equippedArmor.Value); armor.IsEquipped = true; _equippedArmor.OnNext(armor); } else if (equipable is Accessory accessory) { Unequip(_equippedAccessory.Value); accessory.IsEquipped = true; _equippedAccessory.OnNext(accessory); } else throw new NotImplementedException("Item type is not supported."); } public void Unequip(EquipableItem equipable) { if (equipable is Weapon weapon) { weapon.IsEquipped = false; ModifyBonusAttack(-weapon.Damage); _equippedWeapon.OnNext(new Weapon()); } else if (equipable is Armor armor) { armor.IsEquipped = false; ModifyBonusDefense(-armor.Defense); _equippedArmor.OnNext(new Armor()); } else if (equipable is Accessory accessory) { accessory.IsEquipped = false; ModifyMaximumHP(-accessory.MaxHPUp); ModifyMaximumVT(-accessory.MaxVTUp); ModifyBonusAttack(-accessory.ATKUp); ModifyBonusDefense(-accessory.DEFUp); ModifyBonusLuck(-accessory.LuckUp); _equippedAccessory.OnNext(new Accessory()); } else throw new NotImplementedException("Item type is not supported."); } 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 { get { return Input.GetActionStrength(GameInputs.StrafeLeft); } } private static float RightStrafeInputVector { get { return Input.GetActionStrength(GameInputs.StrafeRight); } } 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() { var attackSpeed = ((Weapon)EquippedWeapon.Value).AttackSpeed; AnimationPlayer.SetSpeedScale((float)attackSpeed); if (EquippedWeapon.Value.ItemName == "Atonement") AnimationPlayer.Play("atonement_attack"); else AnimationPlayer.Play("attack"); _gameRepo.OnPlayerAttack(); } private void OnAnimationFinished(StringName animation) { PlayerLogic.Input(new PlayerLogic.Input.AttackAnimationFinished()); } private void OnExitTree() { PlayerLogic.Stop(); PlayerBinding.Dispose(); AnimationPlayer.AnimationFinished -= OnAnimationFinished; } private void OnPlayerPositionUpdated(Vector3 globalPosition) => GlobalPosition = globalPosition; private void OnHealthTimerTimeout() { if (Stats.CurrentHP.Value <= 0) return; if (Stats.CurrentVT.Value > 0) { if (((Accessory)EquippedAccessory.Value).AccessoryTag == AccessoryTag.HalfVTConsumption) { reduceOnTick = !reduceOnTick; } Stats.SetCurrentHP(Stats.CurrentHP.Value + 1); if (reduceOnTick) Stats.SetCurrentVT(Stats.CurrentVT.Value - 1); } else Stats.SetCurrentHP(Stats.CurrentHP.Value - 1); } private void EquippedWeapon_Sync(EquipableItem obj) { ModifyBonusAttack(((Weapon)obj).Damage); } private void EquippedArmor_Sync(EquipableItem obj) { ModifyBonusDefense(((Armor)obj).Defense); } private void EquippedAccessory_Sync(EquipableItem accessory) { ModifyMaximumHP(((Accessory)accessory).MaxHPUp); ModifyMaximumVT(((Accessory)accessory).MaxVTUp); ModifyBonusAttack(((Accessory)accessory).ATKUp); ModifyBonusDefense(((Accessory)accessory).DEFUp); ModifyBonusLuck(((Accessory)accessory).LuckUp); } private void CurrentHP_Sync(int newHealth) { if (newHealth <= 0) Die(); } private void CurrentEXP_Sync(double newExp) { if (Stats.CurrentExp.Value >= Stats.ExpToNextLevel.Value) LevelUp(); } private void Hitbox_AreaEntered(Area3D area) { var target = area.GetOwner(); if (target is IEnemy enemy) HitEnemy(enemy); } private void HitEnemy(IEnemy enemy) { var attackValue = Stats.CurrentAttack.Value + Stats.BonusAttack.Value; var ignoreElementalResistance = ((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.IgnoreAffinity; var isCriticalHit = BattleExtensions.IsCriticalHit(Stats.Luck.Value); var element = ((Weapon)EquippedWeapon.Value).WeaponElement; enemy.TakeDamage( attackValue * ((Weapon)EquippedWeapon.Value).ElementalDamageBonus, element, isCriticalHit, false, ignoreElementalResistance); if (((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.Knockback) enemy.Knockback(0.3f, -CurrentBasis.Z.Normalized()); _gameRepo.OnPlayerAttackedEnemy(); } private void CollisionDetector_AreaEntered(Area3D area) { if (area.GetParent() is InventoryItem inventoryItem) { var isAdded = Inventory.TryAdd(inventoryItem); if (isAdded) { _gameRepo.AnnounceMessageOnMainScreen($"{inventoryItem.ItemName} picked up."); inventoryItem.QueueFree(); } else _gameRepo.AnnounceMessageOnMainScreen($"Could not pick up {inventoryItem.ItemName}."); } if (area.GetParent() is DroppedItem droppedItem) { var isAdded = Inventory.TryAdd(droppedItem.Item); if (isAdded) { _gameRepo.AnnounceMessageOnMainScreen($"{droppedItem.Item.ItemName} picked up."); droppedItem.QueueFree(); } else _gameRepo.AnnounceMessageOnMainScreen($"Could not pick up {droppedItem.Item.ItemName}."); } if (area.GetParent() is ThrownItem thrownItem) { var isAdded = Inventory.TryAdd(thrownItem.ItemThatIsThrown); if (isAdded) { _gameRepo.AnnounceMessageOnMainScreen($"{thrownItem.ItemThatIsThrown.ItemName} picked up."); thrownItem.QueueFree(); } else _gameRepo.AnnounceMessageOnMainScreen($"Could not pick up {thrownItem.ItemThatIsThrown.ItemName}."); } if (area.GetParent() is Restorative restorative) { _gameRepo.OnRestorativePickedUp(restorative); restorative.QueueFree(); } } private PlayerStats InitializePlayerStats() { var playerStats = new PlayerStats( currentHP: new AutoProp(_playerStatResource.CurrentHP), maximumHP: new AutoProp(_playerStatResource.MaximumHP), currentVT: new AutoProp(_playerStatResource.CurrentVT), maximumVT: new AutoProp(_playerStatResource.MaximumVT), currentAttack: new AutoProp(_playerStatResource.CurrentAttack), currentDefense: new AutoProp(_playerStatResource.CurrentDefense), maxAttack: new AutoProp(_playerStatResource.MaxAttack), maxDefense: new AutoProp(_playerStatResource.MaxDefense), bonusAttack: new AutoProp(_playerStatResource.BonusAttack), bonusDefense: new AutoProp(_playerStatResource.BonusDefense), currentExp: new AutoProp(_playerStatResource.CurrentExp), expToNextLevel: new AutoProp(_playerStatResource.ExpToNextLevel), currentLevel: new AutoProp(_playerStatResource.CurrentLevel), luck: new AutoProp(_playerStatResource.Luck)); return playerStats; } private bool PlayerIsHittingGeometry() { var collisions = WallCheck.GetCollidingBodies(); return collisions.Count > 0; } private void WallCheck_BodyEntered(Node body) { PlayerLogic.Input(new PlayerLogic.Input.AttackAnimationFinished()); GD.Print("Hit wall"); AnimationPlayer.Stop(); } }