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 { PlayerStatInfo PlayerStatInfo { get; } PlayerLogic PlayerLogic { get; } PlayerData PlayerData { get; } public Vector3 GetGlobalInputVector(); } [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>(); /// Rotation speed (quaternions?/sec). [Export(PropertyHint.Range, "0, 100, 0.1")] public float RotationSpeed { get; set; } = 12.0f; /// Player speed (meters/sec). [Export(PropertyHint.Range, "0, 100, 0.1")] public float MoveSpeed { get; set; } = 8f; /// Player speed (meters^2/sec). [Export(PropertyHint.Range, "0, 100, 0.1")] public float Acceleration { get; set; } = 4f; [Export] public PlayerStatInfo PlayerStatInfo { get; set; } public PlayerLogic.Settings Settings { get; set; } = default!; public PlayerLogic PlayerLogic { get; set; } = default!; public PlayerData PlayerData { 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 Label HPNumber { get; set; } = default!; [Node] public Label VTNumber { get; set; } = default!; [Node] public ProgressBar HPBar { get; set; } = default!; [Node] public ProgressBar VTBar { get; set; } = default!; [Node] public IArea3D CollisionDetector { get; set; } = default!; private AutoProp _currentHP { get; set; } = default!; private AutoProp _currentVT { get; set; } = default!; public void Initialize() { AnimationPlayer.AnimationFinished += OnAnimationFinished; } public void Setup() { Settings = new PlayerLogic.Settings(RotationSpeed, MoveSpeed); PlayerLogic = new PlayerLogic(); PlayerLogic.Set(this as IPlayer); PlayerLogic.Set(Settings); PlayerLogic.Set(AppRepo); PlayerLogic.Set(GameRepo); PlayerLogic.Set(PlayerData); GameRepo.SetPlayerGlobalPosition(GlobalPosition); GameRepo.PlayerGlobalPosition.Sync += OnPlayerPositionUpdated; _currentHP = new AutoProp(PlayerStatInfo.MaximumHP); _currentVT = new AutoProp(PlayerStatInfo.MaximumVT); _currentHP.Sync += OnHPChanged; _currentVT.Sync += OnVTChanged; HPBar.MaxValue = PlayerStatInfo.MaximumHP; HPBar.Value = _currentHP.Value; VTBar.MaxValue = PlayerStatInfo.MaximumVT; VTBar.Value = _currentVT.Value; HealthTimer.Timeout += OnHealthTimerTimeout; CollisionDetector.AreaEntered += OnEnemyHitBoxEntered; } 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 weaponInfo = (WeaponInfo)GameRepo.EquippedWeapon.Info; var attackSpeed = (float)weaponInfo.AttackSpeed; AnimationPlayer.SetSpeedScale(attackSpeed); AnimationPlayer.Play("attack"); if (weaponInfo.WeaponTags.Contains(WeaponTag.SelfDamage)) _currentHP.OnNext(_currentHP.Value - 5); }) .Handle((in PlayerLogic.Output.ThrowItem output) => { ThrowItem(); }); this.Provide(); PlayerLogic.Start(); SwordSlashAnimation.Position = GetViewport().GetVisibleRect().Size / 2; } public void OnReady() { SetPhysicsProcess(true); } private void OnEnemyHitBoxEntered(Area3D area) { if (area is IHitbox hitBox) { if (_currentHP.Value > 0) { var enemy = hitBox.GetParent(); var isCriticalHit = false; var rng = new RandomNumberGenerator(); rng.Randomize(); var roll = rng.Randf(); if (roll <= enemy.EnemyStatInfo.Luck) isCriticalHit = true; var damage = DamageCalculator.CalculateEnemyDamage(hitBox.Damage, PlayerStatInfo, enemy.EnemyStatInfo, GameRepo.EquippedArmor.ArmorInfo, isCriticalHit); _currentHP.OnNext(_currentHP.Value - damage); GD.Print($"Player hit for {damage} damage."); } } } public void OnPhysicsProcess(double delta) { PlayerLogic.Input(new PlayerLogic.Input.PhysicsTick(delta)); var attackIsPressed = Input.IsActionJustPressed(GameInputs.Attack); if (attackIsPressed && !GameRepo.IsWithinDialogueSpace) PlayerLogic.Input(new PlayerLogic.Input.Attack()); MoveAndSlide(); PlayerLogic.Input(new PlayerLogic.Input.Moved(GlobalPosition)); } 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 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; } private void OnEquippedWeaponChanged(Weapon info) => Hitbox.Damage = ((WeaponInfo)info.Info).Damage; private void OnHPChanged(double newHP) { HPNumber.Text = Mathf.RoundToInt(newHP).ToString(); HPBar.Value = newHP; if (newHP <= 0.0) PlayerLogic.Input(new PlayerLogic.Input.Killed()); } private void OnVTChanged(int newVT) { VTNumber.Text = newVT.ToString(); VTBar.Value = newVT; } private void OnPlayerPositionUpdated(Vector3 globalPosition) => GlobalPosition = globalPosition; private void OnHealthTimerTimeout() { if (_currentVT.Value > 0) _currentVT.OnNext(_currentVT.Value - 1); else _currentHP.OnNext(_currentHP.Value - 1); } } }