using Chickensoft.AutoInject; using Chickensoft.Collections; using Chickensoft.GodotNodeInterfaces; using Chickensoft.Introspection; using Chickensoft.LogicBlocks; using Chickensoft.SaveFileBuilder; using Godot; namespace GameJamDungeon { public interface IPlayer : ICharacterBody3D, IKillable { PlayerLogic PlayerLogic { get; } public Vector3 GetGlobalInputVector(); public float GetLeftStrafeInputVector(); public float GetRightStrafeInputVector(); event Player.InventoryButtonPressedEventHandler InventoryButtonPressed; event Player.MinimapButtonHeldEventHandler MinimapButtonHeld; event Player.PauseButtonPressedEventHandler PauseButtonPressed; } [Meta(typeof(IAutoNode))] public partial class Player : CharacterBody3D, IPlayer, IProvide { public override void _Notification(int what) => this.Notify(what); PlayerLogic IProvide.Value() => PlayerLogic; [Dependency] public IAppRepo AppRepo => this.DependOn(); [Dependency] public IGameRepo GameRepo => this.DependOn(); [Dependency] public ISaveChunk GameChunk => this.DependOn>(); [Signal] public delegate void InventoryButtonPressedEventHandler(); [Signal] public delegate void MinimapButtonHeldEventHandler(); [Signal] public delegate void PauseButtonPressedEventHandler(); [Export] public PlayerStatResource PlayerStatResource { get; set; } = default!; public PlayerLogic.Settings Settings { get; set; } = default!; public PlayerLogic PlayerLogic { get; set; } = default!; public PlayerLogic.IBinding PlayerBinding { get; set; } = default!; [Node] public IAnimationPlayer AnimationPlayer { get; set; } = default!; [Node] public AnimatedSprite2D SwordSlashAnimation { get; set; } = default!; [Node] public IHitbox Hitbox { get; set; } = default!; [Node] public Timer HealthTimer { get; set; } = default!; [Node] public IArea3D CollisionDetector { get; set; } = default!; private PlayerData PlayerData { get; set; } = default!; public void Initialize() { AnimationPlayer.AnimationFinished += OnAnimationFinished; } public void Setup() { Settings = new PlayerLogic.Settings() { RotationSpeed = PlayerStatResource.RotationSpeed, MoveSpeed = PlayerStatResource.MoveSpeed, Acceleration = PlayerStatResource.Acceleration }; PlayerData = new PlayerData() { GlobalTransform = GlobalTransform, StateMachine = PlayerLogic, Velocity = Velocity, Inventory = new Inventory(), }; PlayerData.SetCurrentHP(PlayerStatResource.CurrentHP); PlayerData.SetMaximumHP(PlayerStatResource.MaximumHP); PlayerData.SetCurrentVT(PlayerStatResource.CurrentVT); PlayerData.SetMaximumVT(PlayerStatResource.MaximumVT); PlayerData.SetCurrentAttack(PlayerStatResource.CurrentAttack); PlayerData.SetMaxAttack(PlayerStatResource.MaxAttack); PlayerData.SetCurrentDefense(PlayerStatResource.CurrentDefense); PlayerData.SetMaxDefense(PlayerStatResource.MaxDefense); PlayerData.SetCurrentExp(PlayerStatResource.CurrentExp); PlayerData.SetCurrentLevel(PlayerStatResource.CurrentLevel); PlayerData.SetExpToNextLevel(PlayerStatResource.ExpToNextLevel); PlayerData.SetLuck(PlayerStatResource.Luck); PlayerLogic = new PlayerLogic(); PlayerLogic.Set(this as IPlayer); PlayerLogic.Set(Settings); PlayerLogic.Set(AppRepo); PlayerLogic.Set(GameRepo); PlayerLogic.Set(PlayerData); PlayerData.Inventory.EquippedAccessory.Sync += EquippedAccessory_Sync; PlayerData.CurrentHP.Sync += CurrentHP_Sync; } public void OnResolved() { PlayerBinding = PlayerLogic.Bind(); PlayerBinding .Handle((in PlayerLogic.Output.MovementComputed output) => { Transform = Transform with { Basis = output.Rotation }; Velocity = output.Velocity; }) .Handle((in PlayerLogic.Output.Animations.Attack output) => { var attackSpeed = PlayerData.Inventory.EquippedWeapon.Value.WeaponStats.AttackSpeed; AnimationPlayer.SetSpeedScale((float)attackSpeed); AnimationPlayer.Play("attack"); }) .Handle((in PlayerLogic.Output.ThrowItem output) => { }); this.Provide(); PlayerLogic.Start(); GameRepo.SetPlayerData(PlayerData); SwordSlashAnimation.Position = GetViewport().GetVisibleRect().Size / 2; GlobalPosition = GameRepo.PlayerGlobalPosition.Value; GameRepo.PlayerGlobalPosition.Sync += PlayerGlobalPosition_Sync; HealthTimer.Timeout += OnHealthTimerTimeout; CollisionDetector.AreaEntered += OnEnemyHitBoxEntered; PlayerData.Inventory.AccessoryUnequipped += Inventory_AccessoryUnequipped; } public void OnReady() { SetPhysicsProcess(true); } public override void _UnhandledInput(InputEvent @event) { if (@event.IsActionPressed(GameInputs.Inventory)) { GD.Print("Inventory button pressed"); EmitSignal(SignalName.InventoryButtonPressed); } if (@event.IsActionPressed(GameInputs.MiniMap)) { GD.Print("MiniMap button pressed"); EmitSignal(SignalName.MinimapButtonHeld); } if (@event.IsActionPressed(GameInputs.Pause)) { GD.Print("Pause button pressed"); EmitSignal(SignalName.PauseButtonPressed); } if (@event.IsActionPressed(GameInputs.Attack)) PlayerLogic.Input(new PlayerLogic.Input.Attack()); if (@event.IsActionPressed(GameInputs.Sprint)) Settings.MoveSpeed *= 4; if (@event.IsActionReleased(GameInputs.Sprint)) Settings.MoveSpeed /= 4; } public void OnPhysicsProcess(double delta) { PlayerLogic.Input(new PlayerLogic.Input.PhysicsTick(delta)); MoveAndSlide(); PlayerLogic.Input(new PlayerLogic.Input.Moved(GlobalPosition, GlobalTransform)); } public Vector3 GetGlobalInputVector() { 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 }; } public float GetLeftStrafeInputVector() { var leftStrafe = Input.GetActionStrength(GameInputs.StrafeLeft); return leftStrafe; } public float GetRightStrafeInputVector() { var rightStrafe = Input.GetActionStrength(GameInputs.StrafeRight); return rightStrafe; } public void ThrowItem() { var itemScene = GD.Load("res://src/items/throwable/ThrowableItem.tscn"); var throwItem = itemScene.Instantiate(); GetTree().Root.AddChildEx(throwItem); throwItem.GlobalPosition = GameRepo.PlayerGlobalPosition.Value; throwItem.GlobalRotation = GlobalRotation; throwItem.AnimationPlayer.Play("throw"); } public void OnAnimationFinished(StringName animation) { GD.Print("Attack finished"); PlayerLogic.Input(new PlayerLogic.Input.AttackAnimationFinished()); } public void OnExitTree() { PlayerLogic.Stop(); PlayerBinding.Dispose(); AnimationPlayer.AnimationFinished -= OnAnimationFinished; } public void Kill() => PlayerLogic.Input(new PlayerLogic.Input.Killed()); private void PlayerGlobalPosition_Sync(Vector3 newPlayerPosition) { GlobalPosition = newPlayerPosition; } private void OnPlayerPositionUpdated(Vector3 globalPosition) => GlobalPosition = globalPosition; private void OnHealthTimerTimeout() { if (PlayerData.CurrentVT.Value > 0) PlayerData.SetCurrentVT(PlayerData.CurrentVT.Value - 1); else PlayerData.SetCurrentHP(PlayerData.CurrentHP.Value - 1); } private void EquippedAccessory_Sync(Accessory equippedItem) { PlayerData.SetMaximumHP(PlayerData.MaximumHP.Value + equippedItem.AccessoryStats.MaxHPUp); PlayerData.SetMaximumVT(PlayerData.MaximumVT.Value + equippedItem.AccessoryStats.MaxVTUp); PlayerData.SetLuck(PlayerData.Luck.Value + equippedItem.AccessoryStats.LUCKUp); } private void Inventory_AccessoryUnequipped(AccessoryStats unequippedAccessory) { PlayerData.SetMaximumHP(PlayerData.MaximumHP.Value - unequippedAccessory.MaxHPUp); PlayerData.SetMaximumVT(PlayerData.MaximumVT.Value - unequippedAccessory.MaxVTUp); PlayerData.SetLuck(PlayerData.Luck.Value - unequippedAccessory.LUCKUp); } private void OnEnemyHitBoxEntered(Area3D area) { if (area is IHitbox hitBox) { var enemy = hitBox.GetParent(); var isCriticalHit = false; var rng = new RandomNumberGenerator(); rng.Randomize(); var roll = rng.Randf(); if (roll <= enemy.EnemyStatResource.Luck) isCriticalHit = true; var damage = DamageCalculator.CalculateEnemyDamage(PlayerData.CurrentDefense.Value + PlayerData.BonusDefense, enemy.EnemyStatResource, GameRepo.PlayerData.Inventory.EquippedArmor.Value.ArmorStats, isCriticalHit); PlayerData.SetCurrentHP(PlayerData.CurrentHP.Value - Mathf.RoundToInt(damage)); GD.Print($"Player hit for {damage} damage."); } } private void CurrentHP_Sync(int newHealth) { if (newHealth <= 0) Kill(); } } }