450 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C#
		
	
			
		
		
	
	
			450 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C#
		
	
using Godot;
 | 
						|
using GodotUtilities;
 | 
						|
using SupaLidlGame.Extensions;
 | 
						|
using SupaLidlGame.Items;
 | 
						|
using SupaLidlGame.Utils;
 | 
						|
using SupaLidlGame.State.Character;
 | 
						|
 | 
						|
namespace SupaLidlGame.Characters;
 | 
						|
 | 
						|
public partial class Character : CharacterBody2D, IFaction
 | 
						|
{
 | 
						|
    [Export]
 | 
						|
    public float Speed { get; protected set; } = 32.0f;
 | 
						|
 | 
						|
    [Export]
 | 
						|
    public float Friction { get; protected set; } = 4.0f;
 | 
						|
 | 
						|
    [Export]
 | 
						|
    public float Mass
 | 
						|
    {
 | 
						|
        get => _mass;
 | 
						|
        set
 | 
						|
        {
 | 
						|
            if (value > 0)
 | 
						|
                _mass = value;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    [Signal]
 | 
						|
    public delegate void HealthChangedEventHandler(Events.HealthChangedArgs args);
 | 
						|
 | 
						|
    [Signal]
 | 
						|
    public delegate void HurtEventHandler(Events.HurtArgs args);
 | 
						|
 | 
						|
    [Signal]
 | 
						|
    public delegate void DeathEventHandler(Events.HurtArgs args);
 | 
						|
 | 
						|
    protected float _mass = 1.0f;
 | 
						|
 | 
						|
    public Vector2 NetImpulse { get; set; } = Vector2.Zero;
 | 
						|
 | 
						|
    public Vector2 Direction { get; set; } = Vector2.Zero;
 | 
						|
 | 
						|
    public Vector2 Target { get; set; } = Vector2.Zero;
 | 
						|
    
 | 
						|
    [Export]
 | 
						|
    public Texture2D HandTexture { get; set; }
 | 
						|
 | 
						|
    [Export]
 | 
						|
    public virtual float Health
 | 
						|
    {
 | 
						|
        get => _health;
 | 
						|
        set
 | 
						|
        {
 | 
						|
            if (!IsAlive && value < 0)
 | 
						|
            {
 | 
						|
                return;
 | 
						|
            }
 | 
						|
 | 
						|
            var args = new Events.HealthChangedArgs
 | 
						|
            {
 | 
						|
                OldHealth = _health,
 | 
						|
                NewHealth = value,
 | 
						|
            };
 | 
						|
            EmitSignal(SignalName.HealthChanged, args);
 | 
						|
 | 
						|
            _health = value;
 | 
						|
            if (_health <= 0)
 | 
						|
            {
 | 
						|
                Die();
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public bool IsAlive => Health > 0;
 | 
						|
 | 
						|
    protected float _health = 100f;
 | 
						|
 | 
						|
    public double StunTime { get; set; }
 | 
						|
 | 
						|
    [Export]
 | 
						|
    public Sprite2D Sprite { get; set; }
 | 
						|
 | 
						|
    [Export]
 | 
						|
    public Inventory Inventory { get; set; }
 | 
						|
 | 
						|
    [Export]
 | 
						|
    public CharacterStateMachine StateMachine { get; set; }
 | 
						|
 | 
						|
    [Export]
 | 
						|
    public BoundingBoxes.Hurtbox Hurtbox { get; set; }
 | 
						|
 | 
						|
    [Export]
 | 
						|
    public ushort Faction { get; set; }
 | 
						|
 | 
						|
    public AnimationPlayer MovementAnimation { get; set; }
 | 
						|
 | 
						|
    public AnimationPlayer HurtAnimation { get; set; }
 | 
						|
 | 
						|
    public AnimationPlayer StunAnimation { get; set; }
 | 
						|
 | 
						|
    public AnimationPlayer AttackAnimation { get; set; }
 | 
						|
 | 
						|
    private UI.DamageText _curDamageText;
 | 
						|
 | 
						|
    public override void _Ready()
 | 
						|
    {
 | 
						|
        // TODO: 80+ char line
 | 
						|
        MovementAnimation = GetNode<AnimationPlayer>("Animations/Movement");
 | 
						|
        HurtAnimation = GetNode<AnimationPlayer>("Animations/Hurt");
 | 
						|
        StunAnimation = GetNode<AnimationPlayer>("Animations/Stun");
 | 
						|
        AttackAnimation = GetNode<AnimationPlayer>("Animations/Attack");
 | 
						|
 | 
						|
        Hurtbox.ReceivedDamage += OnReceivedDamage;
 | 
						|
    }
 | 
						|
 | 
						|
    public override void _Process(double delta)
 | 
						|
    {
 | 
						|
        if (StateMachine != null)
 | 
						|
        {
 | 
						|
            StateMachine.Process(delta);
 | 
						|
        }
 | 
						|
 | 
						|
        if (StunTime > 0 && !StunAnimation.IsPlaying())
 | 
						|
        {
 | 
						|
            StunAnimation.Play("stun");
 | 
						|
        }
 | 
						|
        else if (StunTime < 0 && StunAnimation.IsPlaying())
 | 
						|
        {
 | 
						|
            StunAnimation.Stop();
 | 
						|
        }
 | 
						|
 | 
						|
        if (Target.X < 0)
 | 
						|
        {
 | 
						|
            Sprite.FlipH = true;
 | 
						|
        }
 | 
						|
        else if (Target.X > 0)
 | 
						|
        {
 | 
						|
            Sprite.FlipH = false;
 | 
						|
        }
 | 
						|
        DrawTarget();
 | 
						|
    }
 | 
						|
 | 
						|
    public override void _PhysicsProcess(double delta)
 | 
						|
    {
 | 
						|
        if (StateMachine != null)
 | 
						|
        {
 | 
						|
            StateMachine.PhysicsProcess(delta);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Modify the <c>Character</c>'s velocity
 | 
						|
    /// </summary>
 | 
						|
    public virtual void ModifyVelocity()
 | 
						|
    {
 | 
						|
        if (StunTime > 0)
 | 
						|
        {
 | 
						|
            Velocity *= 0.25f;
 | 
						|
        }
 | 
						|
 | 
						|
        var state = StateMachine.CurrentState;
 | 
						|
        if (state is State.Character.CharacterDashState dashState)
 | 
						|
        {
 | 
						|
            Velocity *= dashState.VelocityModifier;
 | 
						|
        }
 | 
						|
        // TODO: make PlayerRollState a CharacterRollState instead
 | 
						|
        else if (state is State.Character.PlayerRollState rollState)
 | 
						|
        {
 | 
						|
            Velocity *= 2;
 | 
						|
            //Velocity *= rollState.VelocityModifier;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Handles the <c>Character</c>'s death.
 | 
						|
    /// </summary>
 | 
						|
    public virtual void Die()
 | 
						|
    {
 | 
						|
        if (HurtAnimation.HasAnimation("death"))
 | 
						|
        {
 | 
						|
            HurtAnimation.Play("death");
 | 
						|
            HurtAnimation.AnimationFinished += (StringName name) =>
 | 
						|
                QueueFree();
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            QueueFree();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public void ApplyImpulse(Vector2 impulse, bool resetVelocity = false)
 | 
						|
    {
 | 
						|
        // delta p = F delta t
 | 
						|
        if (resetVelocity)
 | 
						|
            Velocity = Vector2.Zero;
 | 
						|
        NetImpulse += impulse / Mass;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Stuns the <c>Chararacter</c> for an amount of time. If
 | 
						|
    /// <paramref name="time"/> is less than the <c>Character</c>'s current
 | 
						|
    /// stun time left, it will have no effect.
 | 
						|
    /// </summary>
 | 
						|
    public virtual void Stun(float time)
 | 
						|
    {
 | 
						|
        StunTime = Mathf.Max(time, StunTime);
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Draws the character so that its sprite and inventory items face the
 | 
						|
    /// character's direction.
 | 
						|
    /// </summary>
 | 
						|
    protected virtual void DrawTarget()
 | 
						|
    {
 | 
						|
        Vector2 target = Target;
 | 
						|
        float angle = Mathf.Atan2(target.Y, Mathf.Abs(target.X));
 | 
						|
        Vector2 scale = Inventory.Scale;
 | 
						|
        if (target.X < 0)
 | 
						|
        {
 | 
						|
            scale.Y = -1;
 | 
						|
            angle = Mathf.Pi - angle;
 | 
						|
        }
 | 
						|
        else if (target.X > 0)
 | 
						|
        {
 | 
						|
            scale.Y = 1;
 | 
						|
        }
 | 
						|
        Inventory.Scale = scale;
 | 
						|
        Inventory.Rotation = angle;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Use the current item the character is using. Prefer to call this over
 | 
						|
    /// <c>Item.Use</c> as it will check if the character is stunned or alive.
 | 
						|
    /// </summary>
 | 
						|
    public void UseCurrentItem()
 | 
						|
    {
 | 
						|
        if (StunTime > 0 || !IsAlive)
 | 
						|
        {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (Inventory.SelectedItem is Weapon weapon)
 | 
						|
        {
 | 
						|
            weapon.Use();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public void DeuseCurrentItem()
 | 
						|
    {
 | 
						|
        if (Inventory.SelectedItem is Weapon weapon)
 | 
						|
        {
 | 
						|
            weapon.Deuse();
 | 
						|
            // TODO: DeusedItem signal, implement when needed
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public void UseCurrentItemAlt()
 | 
						|
    {
 | 
						|
        if (StunTime > 0 || !IsAlive)
 | 
						|
        {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (Inventory.SelectedItem is Weapon weapon)
 | 
						|
        {
 | 
						|
            weapon.UseAlt();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public void DeuseCurrentItemAlt()
 | 
						|
    {
 | 
						|
        if (Inventory.SelectedItem is Weapon weapon)
 | 
						|
        {
 | 
						|
            weapon.DeuseAlt();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public void LookTowardsDirection()
 | 
						|
    {
 | 
						|
        if (!Direction.IsZeroApprox())
 | 
						|
        {
 | 
						|
            Target = Direction;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Override this method to modify the damage the character takes.
 | 
						|
    /// </summary>
 | 
						|
    protected virtual float ReceiveDamage(
 | 
						|
        float damage,
 | 
						|
        Character inflictor,
 | 
						|
        float knockback,
 | 
						|
        Vector2 knockbackDir = default) => damage;
 | 
						|
 | 
						|
    protected void CreateDamageText(float damage)
 | 
						|
    {
 | 
						|
        // create damage text
 | 
						|
        var textScene = GD.Load<PackedScene>("res://UI/DamageText.tscn");
 | 
						|
        if (_curDamageText is null || !IsInstanceValid(_curDamageText))
 | 
						|
        {
 | 
						|
            _curDamageText = textScene.Instantiate<UI.DamageText>();
 | 
						|
            this.GetWorld().CurrentMap.AddChild(_curDamageText);
 | 
						|
        }
 | 
						|
        _curDamageText.Damage += damage;
 | 
						|
        _curDamageText.Timer.Start();
 | 
						|
        _curDamageText.GlobalPosition = GlobalPosition;
 | 
						|
        _curDamageText.ShowText();
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Handles the character taking damage.
 | 
						|
    /// </summary>
 | 
						|
    protected virtual void OnReceivedDamage(
 | 
						|
        float damage,
 | 
						|
        Character inflictor,
 | 
						|
        float knockback,
 | 
						|
        Weapon weapon = null,
 | 
						|
        Vector2 knockbackDir = default)
 | 
						|
    {
 | 
						|
        if (Health <= 0)
 | 
						|
        {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        float oldHealth = Health;
 | 
						|
        damage = ReceiveDamage(damage, inflictor, knockback, knockbackDir);
 | 
						|
        Health -= damage;
 | 
						|
 | 
						|
        var hurtParticles = GetNode<GpuParticles2D>("Effects/HurtParticles");
 | 
						|
        if (hurtParticles is not null)
 | 
						|
        {
 | 
						|
            hurtParticles.SetDirection(knockbackDir);
 | 
						|
        }
 | 
						|
 | 
						|
        CreateDamageText(damage);
 | 
						|
 | 
						|
        // apply knockback
 | 
						|
        ApplyImpulse(knockbackDir.Normalized() * knockback);
 | 
						|
 | 
						|
        // play damage animation
 | 
						|
        if (HurtAnimation is not null && Health > 0)
 | 
						|
        {
 | 
						|
            HurtAnimation.Stop();
 | 
						|
            HurtAnimation.Play("hurt");
 | 
						|
            if (HurtAnimation.HasAnimation("hurt_flash"))
 | 
						|
            {
 | 
						|
                HurtAnimation.Queue("hurt_flash");
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (this.GetNode("Effects/HurtSound") is AudioStreamPlayer2D sound)
 | 
						|
        {
 | 
						|
            // very small pitch deviation
 | 
						|
            sound.At(GlobalPosition).WithPitchDeviation(0.125f).PlayOneShot();
 | 
						|
        }
 | 
						|
 | 
						|
        Events.HurtArgs args = new Events.HurtArgs
 | 
						|
        {
 | 
						|
            Attacker = inflictor,
 | 
						|
            OldHealth = oldHealth,
 | 
						|
            NewHealth = Health,
 | 
						|
            Weapon = weapon,
 | 
						|
            Damage = damage,
 | 
						|
        };
 | 
						|
 | 
						|
        EmitSignal(SignalName.Hurt, args);
 | 
						|
 | 
						|
        if (inflictor is Player)
 | 
						|
        {
 | 
						|
            EmitPlayerHitSignal(args);
 | 
						|
        }
 | 
						|
 | 
						|
        if (Health <= 0)
 | 
						|
        {
 | 
						|
            EmitSignal(SignalName.Death, args);
 | 
						|
            GetNode<GpuParticles2D>("DeathParticles")?
 | 
						|
                .CloneOnWorld<GpuParticles2D>()
 | 
						|
                .EmitOneShot();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Converts a HurtArgs to HitArgs if the attacker was a player and emits
 | 
						|
    /// the <c>EventBus.PlayerHit</c> signal.
 | 
						|
    /// </summary>
 | 
						|
    private void EmitPlayerHitSignal(Events.HurtArgs args)
 | 
						|
    {
 | 
						|
        var newArgs = new Events.HitArgs
 | 
						|
        {
 | 
						|
            OldHealth = args.OldHealth,
 | 
						|
            NewHealth = args.NewHealth,
 | 
						|
            Damage = args.Damage,
 | 
						|
            Weapon = args.Weapon,
 | 
						|
            Victim = this,
 | 
						|
        };
 | 
						|
 | 
						|
        var bus = Events.EventBus.Instance;
 | 
						|
        bus.EmitSignal(Events.EventBus.SignalName.PlayerHit, newArgs);
 | 
						|
    }
 | 
						|
 | 
						|
#if DEBUG
 | 
						|
    /// <summary>
 | 
						|
    /// For debugging purposes
 | 
						|
    /// </summary>
 | 
						|
    public void Inflict(float damage)
 | 
						|
    {
 | 
						|
        OnReceivedDamage(damage, null, 0);
 | 
						|
    }
 | 
						|
#endif
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Plays a footstep sound. This should be called through an
 | 
						|
    /// <c>AnimationPlayer</c> to sync sounds with animations.
 | 
						|
    /// </summary>
 | 
						|
    public virtual void Footstep()
 | 
						|
    {
 | 
						|
        if (GetNode("Effects/Footstep") is AudioStreamPlayer2D player)
 | 
						|
        {
 | 
						|
            player.Play();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Returns whether the <c>Character</c> has line of sight with
 | 
						|
    /// <paramref name="character"/>.
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="character">The character to check for LOS</param>
 | 
						|
    /// <param name="excludeClip">
 | 
						|
    /// Determines whether the raycast should pass through world clips (physics
 | 
						|
    /// layer 5)
 | 
						|
    /// </param>
 | 
						|
    public bool HasLineOfSight(Character character, bool excludeClip = false)
 | 
						|
    {
 | 
						|
        var exclude = new Godot.Collections.Array<Godot.Rid>();
 | 
						|
        exclude.Add(GetRid());
 | 
						|
        var rayParams = new PhysicsRayQueryParameters2D
 | 
						|
        {
 | 
						|
            Exclude = exclude,
 | 
						|
            From = GlobalPosition,
 | 
						|
            To = character.GlobalPosition,
 | 
						|
            //CollisionMask = 1 + (uint)(excludeClip ? 0 : 16),
 | 
						|
            CollisionMask = 1,
 | 
						|
        };
 | 
						|
        var spaceState = GetWorld2D().DirectSpaceState;
 | 
						|
        var result = spaceState.IntersectRay(rayParams);
 | 
						|
        return result.Count == 0;
 | 
						|
    }
 | 
						|
}
 |