using Chickensoft.AutoInject; using Chickensoft.Collections; using Chickensoft.GodotNodeInterfaces; using Chickensoft.Introspection; using Chickensoft.SaveFileBuilder; using Godot; using SimpleInjector; using System; using Zennysoft.Game.Implementation.Components; 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); 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 HealthComponent HealthComponent { get; private set; } public VTComponent VTComponent { get; private set; } public AttackComponent AttackComponent { get; private set; } public DefenseComponent DefenseComponent { get; private set; } public ExperiencePointsComponent ExperiencePointsComponent { get; private set; } public LuckComponent LuckComponent { get; private set; } public Vector3 CurrentPosition => GlobalPosition; public Basis CurrentBasis => Transform.Basis; 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 [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 AnimatedSprite2D SwordSlashAnimation { 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!; #endregion private bool flipAttack = false; private float _healthTimerWaitTime = 3.0f; private bool reduceOnTick = true; private float _knockbackStrength = 0.0f; private Vector3 _knockbackDirection = Vector3.Zero; #region Initialization public void InitializePlayerState() { Inventory = new Inventory(); SetProcessInput(false); SetPhysicsProcess(false); HealthTimer.WaitTime = _healthTimerWaitTime; HealthTimer.Timeout += OnHealthTimerTimeout; } public void Setup() { var container = new SimpleInjector.Container(); container.Register(Lifestyle.Singleton); PlayerLogic = container.GetInstance(); PlayerLogic.Set(this as IPlayer); PlayerLogic.Set(Settings); PlayerLogic.Set(_gameRepo); Hitbox.AreaEntered += Hitbox_AreaEntered; CollisionDetector.AreaEntered += CollisionDetector_AreaEntered; } public void OnResolved() { Settings = new PlayerLogic.Settings() { RotationSpeed = RotationSpeed, MoveSpeed = MoveSpeed, Acceleration = Acceleration }; PlayerChunk = new SaveChunk( onSave: (chunk) => new PlayerData() { Inventory = Inventory }, onLoad: (chunk, data) => { Inventory = data.Inventory; } ); PlayerBinding = PlayerLogic.Bind(); PlayerBinding .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; HealthComponent = new HealthComponent(InitialHP); HealthComponent.HealthReachedZero += Die; VTComponent = new VTComponent(InitialVT); AttackComponent = new AttackComponent(InitialAttack); DefenseComponent = new DefenseComponent(InitialDefense); ExperiencePointsComponent = new ExperiencePointsComponent(); LuckComponent = new LuckComponent(InitialLuck); } #endregion public void Activate() { SetProcessInput(true); SetPhysicsProcess(true); SetHealthTimerStatus(HealthTimerIsActive); } public void Deactivate() { SetProcessInput(false); SetPhysicsProcess(false); SetHealthTimerStatus(false); } public void Attack() { if (PlayerIsHittingGeometry()) { AnimationPlayer.Play("hit_wall"); _gameRepo.OnPlayerAttackedWall(); } else { PlayAttackAnimation(); } } public void SetHealthTimerStatus(bool isActive) { if (isActive) HealthTimer.Start(); else HealthTimer.Stop(); } 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(Damage damage) { var damageReceived = DamageCalculator.CalculateDamage(damage, DefenseComponent.TotalDefense, ((Armor)_equippedArmor.Value).Stats.ElementalResistanceSet); HealthComponent.Damage(damageReceived); } 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; SwordSlashAnimation.Stop(); SetProcessInput(false); SetPhysicsProcess(false); Game.GameOver(); } 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 = 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; _equippedWeapon.OnNext(new Weapon()); } else if (equipable is Armor armor) { armor.IsEquipped = false; _equippedArmor.OnNext(new Armor()); } else if (equipable is Accessory accessory) { accessory.IsEquipped = false; _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 => Input.GetActionStrength(GameInputs.StrafeLeft); private static float RightStrafeInputVector => 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); AnimationPlayer.Play("attack"); _gameRepo.OnPlayerAttack(); } private void OnExitTree() { PlayerLogic.Stop(); PlayerBinding.Dispose(); } private void OnPlayerPositionUpdated(Vector3 globalPosition) => GlobalPosition = globalPosition; private void OnHealthTimerTimeout() { if (VTComponent.CurrentVT.Value > 0) { if (((Accessory)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 = ((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.IgnoreAffinity; var isCriticalHit = BattleExtensions.IsCriticalHit(LuckComponent.Luck.Value); var element = ((Weapon)EquippedWeapon.Value).WeaponElement; var baseAttack = new Damage( (int)(AttackComponent.TotalAttack * ((Weapon)EquippedWeapon.Value).ElementalDamageBonus), element, false, false, ignoreElementalResistance); var damageDealt = DamageCalculator.CalculateDamage(baseAttack, enemy.DefenseComponent.TotalDefense, new ElementalResistanceSet(0, 0, 0, 0, 0)); enemy.TakeDamage(damageDealt); if (((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.Knockback && enemy is IKnockbackable knockbackable) knockbackable.Knockback(0.3f, -CurrentBasis.Z.Normalized()); if (((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.SelfDamage) TakeDamage(new Damage(5, ElementType.None, false, true, true)); _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 bool PlayerIsHittingGeometry() { var collisions = WallCheck.GetCollidingBodies(); return collisions.Count > 0; } private void WallCheck_BodyEntered(Node body) { GD.Print("Hit wall"); AnimationPlayer.Stop(); } }