PlayerController

 avatar
unknown
csharp
a month ago
26 kB
5
Indexable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Grappling;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Rendering.PostProcessing;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class PlayerController : MonoBehaviour
{
    public static PlayerController Singleton;

    [Header("References")]
    [SerializeField] Rigidbody2D rb;
    [HideInInspector] public GrapplingGun grapplingGun;
    [SerializeField] new ParticleSystem particleSystem;
    [SerializeField] PostProcessVolume volume;
    TrailRenderer tr;

    [Header("Stats")]
    
    [SerializeField] bool invincible = false; 
    public bool canAttack { get; private set; }
    public int coins = 0;
    [Tooltip("How much damage the player does")] public float damageMult = 1;
    [Tooltip("How much damage the player takes")] public float takeDamageMult = 1;
    public float critChance;
    [Tooltip("Period after taking damage when you are invincible")] [SerializeField] float invincibilityTime = 0.1f;

    [Header("Bounds")]
    [SerializeField] Color heavenColor;
    [SerializeField] float heavenVignetteIntensity;
    [SerializeField] int heavenSize = 9;
    [SerializeField] float outOfBoundsDamage;
    [SerializeField] float outOfBoundsDamageCooldown;
    [Tooltip("The velocity at which the player will be bounced up if he lands on a barrier")] [SerializeField] float outOfBoundsFallingGraceVelocity;

    [Header("Health Regeneration")]
    [Tooltip("How long it takes until player starts regenerating health after taking daamge")] 
    [SerializeField] float damageRegenCooldown = 3;
    [HideInInspector] public float additionalDamageRegenCooldown;
    [Tooltip("How long it takes for another tick of regeneration to occur")]
    [SerializeField] float regenTickCooldown = 0.25f;
    [Tooltip("How much each tick of regeneration heals for")]
    [SerializeField] public float regenValue = 0.75f;
    [HideInInspector] public float additionalRegenValue;
    [SerializeField] float healVignetteIntensity = 0.5f;
    [SerializeField] float healVignetteFlashTime = 0.15f;
    [SerializeField] ParticleSystem regenParticles;

    [Header("Movement")]
    [SerializeField] float movementSpeed;
    [SerializeField] float friction;
    [SerializeField] float airFriction;
    float _friction;
    [HideInInspector] public float speedMult;
    [SerializeField] float jumpForce;
    [HideInInspector] public float jumpMult;
    [SerializeField] float jumpCooldown = 0.3f;
    [SerializeField] float raycastDist;
    [SerializeField] LayerMask groundLayer;
    [SerializeField] float baseMaxFallSpeed;
    public int maxDoubleJump = 1;
    public bool grounded;
    [SerializeField] float coyoteTime = 0.2f;
    ParticleSystem groundParticles;
    Vector2 groundColliderSize;
    [SerializeField] float footstepsPerSecondPerSpeed;

    [Header("Dashing")]
    [SerializeField] float dashForce;
    public float dashDuration;
    public float dashCooldown;
    [SerializeField] float gravityResetSpeed;

    [Header("Parachute")]
    [HideInInspector] public float maxFallSpeed = -10;
    [HideInInspector] public float gravityReductionMult;

    [Header("Health")]
    [SerializeField] public float maxHealth;
    [SerializeField] static float health;
    float defence;
    [SerializeField] Slider healthbar;

    [Header("Chests")]
    [HideInInspector] public ChestSpawnData chestWithinRangeData;

    [Header("Damage camera shake")]
    [SerializeField] float intensity;
    [SerializeField] float duration;
    [SerializeField] float frequency;

    [Header("Item upgrades")]
    [SerializeField] float sniperScopeIncrement = 0.1f;
    [SerializeField] float bloodyDaggerIncrement = 0.05f;
    float randomBleedChance = 0;
    [SerializeField] float stunPerkIncrement = 0.075f;
    float randomStunChance;
    [SerializeField] float airDefenceIncrement = 0.05f;
    float airDamageReduction;
    [SerializeField] float cactusIncrement = 0.1f;
    [SerializeField] float cactusKnockback = 5;
    float cactus;
    [SerializeField] float stonksIncrease;
    float stonks;

    float _gravity;
    float spawnOffset;
    private void Start()
    {
        canJump = true;

        tr = GetComponent<TrailRenderer>();
        tr.emitting = false;
        groundParticles = transform.GetChild(3).GetChild(3).GetComponent<ParticleSystem>();
        groundColliderSize = GetComponent<BoxCollider2D>().size;

        Singleton = this;

        spawnOffset = FindObjectOfType<TerrainGenerator>().chunkSize / -2;
        transform.position = new Vector2(spawnOffset, spawnOffset);

        healthbar.maxValue = maxHealth;
        health = maxHealth;
        healthbar.value = health;

        speedMult = 1;
        jumpMult = 1;
        canDash = false;
        gravityReductionMult = 1;

        stonks = 1;

        _gravity = rb.gravityScale;

        canAttack = true;

        ResetAllStaticFields(typeof(DataPersistency));

        // Starter items:
        ItemSelectionMenuSystem.Singleton.OpenItemSelectionMenu(UnityEngine.Random.Range(0, 9999), 2, null, ItemSelectionMenuSystem.ItemSelectionType.StarterItemsOnly);

        // Give player pickaxe:
        StartCoroutine(GivePlayerPickaxe());
    }

    private IEnumerator GivePlayerPickaxe()
    {
        yield return null;

        InventoryManager.Singleton.AddItem(0);
    }

    void ResetAllStaticFields(Type targetType)
    {
        // Get all static fields in the class
        FieldInfo[] fields = targetType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);

        int resets = 0;
        foreach (var field in fields)
        {
            if (field.FieldType == typeof(float))
            {
                // Set static float fields to -1
                field.SetValue(null, -1f); // Use 'null' because these are static fields
                resets++;
            }
            else if (field.FieldType == typeof(bool))
            {
                // Set static bool fields to false
                field.SetValue(null, false);
                resets++;
            }
            else if (field.FieldType == typeof(int))
            {
                // Static int
                field.SetValue(null, -1);
                resets++;
            }
        }

        Debug.Log($"Reset {resets} of {fields.Count()} fields.");
    }


    bool canBeHelpedOutOfBounds;
    bool grappling;
    bool invincibleBeforeInHeaven;
    private void Update()
    {
        Movement();
        if (grapplingGun != null) grappling = grapplingGun.grappling;

        // Health regeneration
        PlayerNaturalHealthRegeneration();
        if (naturallyRegening && !regenParticles.isPlaying) regenParticles.Play();
        else if (!naturallyRegening && regenParticles.isPlaying) regenParticles.Stop();

        // Heaven
        float y = transform.position.y;
        bool inHeaven = false;
        if (!enteredHeaven && invincible) invincibleBeforeInHeaven = true;
        for (int i = 0; i < 2; i++)
        {
            if (y < (i + 1) * -200 - heavenSize && y > (i + 1) * -200) {
                inHeaven = true;
                InHeaven();
            }
        }

        // Exit heaven
        if (!inHeaven && enteredHeaven) {
            canAttack = true;
            enteredHeaven = false;
            PlayerVignetteManager.Singleton.RemoveVignette("heaven vignette");
            // Only undo invincibility if we were not invincible before
            if (!invincibleBeforeInHeaven) invincible = false; 
        }

        if (health <= 0) {
            Die();
        }

        if (playerOutOfBounds) {
            if (rb.velocity.y < outOfBoundsFallingGraceVelocity && canBeHelpedOutOfBounds) {
                rb.velocity = new Vector2(rb.velocity.x, 15);
                canBeHelpedOutOfBounds = false;
                return;
            }
            canBeHelpedOutOfBounds = false;

            PlayerOutOfBounds();
        } else if (!playerOutOfBounds) canBeHelpedOutOfBounds = true;

        // Chest
        if (Input.GetKeyDown(KeyCode.E) && withinChestRange)
        {
            if (!itemSelectionMenuOpen)
            {
                int seed = chestWithinRangeData.LootSeed;
                int count = chestWithinRangeData.ItemOptionCount;
                ItemSelectionMenuSystem.Singleton.OpenItemSelectionMenu(seed, count, chestWithinRangeData);
                itemSelectionMenuOpen = true;
            } else {
                ItemSelectionMenuSystem.Singleton.CloseItemSelectionMenu();
                itemSelectionMenuOpen = false;
            }
        }

        DebugLogs();
    }

    private void Die()
    {
        SceneManager.LoadScene(0);
        
        for (int i = 0; i < times.Count; i++)
        {
            Debug.Log($"Stage {i} duration: {TimeSpan.FromSeconds(times[i])}");
        }
    }

    Coroutine footstepCoroutine;
    bool canJump = true;
    [HideInInspector] public int doubleJumps = 0; // Amount of double jumps used in air
    private void Movement()
    {
        // Return if we are dashing
        if (dashing) return;

        // Movement
        float x = Input.GetAxis("Horizontal") * movementSpeed * speedMult;
        bool velocityWithinAffectionRange = rb.velocity.x <= movementSpeed * speedMult + 0.001f && rb.velocity.x >= -movementSpeed * speedMult - 0.001f;
        if (!grappling && !dashing && velocityWithinAffectionRange) rb.velocity = new Vector2(x, rb.velocity.y);
        if (!velocityWithinAffectionRange) {
            if (grounded) 
                _friction = friction;
            else 
                _friction = airFriction;

            ApplyFriction();
        }

        // Ground check
        if (Physics2D.BoxCast(transform.position, new Vector2(groundColliderSize.x / 2, transform.localScale.y), 0, Vector2.down, raycastDist, groundLayer)) 
        {
            defence = 1;
            grounded = true;
            doubleJumps = 0;
            coyoteTimeCoroutine = null;
        } else {
            defence = 1 - airDamageReduction;
            grounded = false;
            if (coyoteTimeCoroutine == null) coyoteTimeCoroutine = StartCoroutine(CoyoteTime());
        }

        // Footsteps
        if (Mathf.Abs(x) > 0.1f && grounded) {
            if (footstepCoroutine == null) footstepCoroutine = StartCoroutine(Footstep());
        }

        // Ground particles
        if (grounded && !groundParticles.isPlaying) groundParticles.Play();
        else if (!grounded && groundParticles.isPlaying) groundParticles.Stop();

        // Double jumping
        if (Input.GetKeyDown(KeyCode.Space) && !grounded && !canJumpCoyoteTime && doubleJumps < maxDoubleJump && canJump)
        {
            doubleJumps++;
            Jump();
        }

        // Jumping
        if (Input.GetKeyDown(KeyCode.Space) && canJump && (grounded || canJumpCoyoteTime))
        {
            Jump();
        }

        if (Input.GetKeyDown(KeyCode.Space) && grappling && grapplingGun != null) grapplingGun.StopGrappling();
    
        Debug.DrawRay(transform.position, Vector2.down * raycastDist, Color.red);

        // Dashing
        if (Input.GetKeyDown(KeyCode.LeftShift) && canDash && !dashing && !grappling) StartCoroutine(Dash());

        // Lerp gravity
        if (lerpGravity) {
            time += Time.deltaTime / gravityResetSpeed;
            gravity = Mathf.Lerp(0, _gravity, time);
            rb.gravityScale = gravity;
            if (gravity >= _gravity) {
                rb.gravityScale = _gravity;
                lerpGravity = false;
                time = 0;
                gravity = 0;
            }
        }
    
        // Gliding / Parachuting
        if (Input.GetKey(KeyCode.LeftShift) && !grounded && rb.velocity.y < 0 && !dashing && gravityReductionMult != 1 && !grappling) {
            rb.gravityScale = _gravity * gravityReductionMult;
            if (rb.velocity.y < maxFallSpeed) rb.velocity = new Vector2(rb.velocity.x, maxFallSpeed);
            if (!tr.emitting) tr.emitting = true;
        } else if (!dashing) {
            rb.gravityScale = _gravity;
            if (tr.emitting) tr.emitting = false;
        }

        if (rb.velocity.y < baseMaxFallSpeed) rb.velocity = new Vector2(rb.velocity.x, baseMaxFallSpeed);

        //if (grounded && !dashing && grappling && !velocityWithinAffectionRange) ApplyFriction();

        // Grappling
        if (Input.GetMouseButtonDown(1)) { // If we right click and we can swap items
            Vector2 dir = transform.position - Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 10));
            if (Physics2D.Raycast(transform.position, -dir.normalized, GrapplingGun.minPullDistance, 1 << 3)) return;

            if (InventoryManager.Singleton.canDeselect && TimingManager.Singleton._CooldownEnded("grappleCooldown"))
            {
                if (!EventSystem.current.IsPointerOverGameObject()) {
                    previousItemGrapple = InventoryManager.Singleton.currentItemSelected;
                    InventoryManager.Singleton.RightClickFavoritedItem(2); // Select grappling gun
                    StartCoroutine(GrappleSetDelay());
                }
            }
        }
    }
    float time = 0;

    private IEnumerator Footstep()
    {
        // Play sound
        int length = SoundManager.Singleton.soundLists[3].sounds.Length;
        int i = UnityEngine.Random.Range(0, length - 1);
        SoundManager.Singleton.PlaySound("Player walking", "", i, 0.1f);

        yield return new WaitForSeconds(footstepsPerSecondPerSpeed / movementSpeed);

        footstepCoroutine = null;
    }

    bool enteredHeaven;
    private void InHeaven()
    {
        if (!enteredHeaven) {
            // Enter heaven
            enteredHeaven = true;
            PlayerVignetteManager.Singleton.RequestVignette(new VignetteRequest(heavenColor, heavenVignetteIntensity, 5), "heaven vignette");
            invincible = true;
            canAttack = false;
        }
    }

    int previousItemGrapple;
    private IEnumerator GrappleSetDelay()
    {
        yield return null;
        if (grapplingGun != null) {
            grapplingGun.previusItemEquiped = previousItemGrapple;
        }
    }

    private void Jump()
    {
        StartCoroutine(_Jump());
    }

    private IEnumerator _Jump()
    {
        rb.velocity = new Vector2(rb.velocity.x, 0);
        rb.AddForce(Vector3.up * jumpForce * 100 * jumpMult);
        if (doubleJumps == 0) particleSystem.transform.GetChild(0).gameObject.SetActive(true);
        else particleSystem.transform.GetChild(0).gameObject.SetActive(false);
        if (!particleSystem.isPlaying) particleSystem.Play();
        canJump = false;     
        StartCoroutine(DustParticles());   

        yield return new WaitForSeconds(jumpCooldown);

        canJump = true;
    }

    bool canJumpCoyoteTime;
    Coroutine coyoteTimeCoroutine;
    private IEnumerator CoyoteTime()
    {
        canJumpCoyoteTime = true;

        yield return new WaitForSeconds(coyoteTime);

        canJumpCoyoteTime = false;
    }

    private IEnumerator DustParticles()
    {
        particleSystem.Play();

        yield return new WaitForSeconds(0.15f);

        particleSystem.Stop();
    }

    public void DamagePlayerTest()
    {
        DamagePlayer(new DamageData(1, 0, Vector2.zero, null));
    }

    Coroutine damageCooldownCoroutine;
    public void DamagePlayer(DamageData damageData)
    {
        if (invincible || damageCooldownCoroutine != null) return;
        damageCooldownCoroutine = StartCoroutine(DamageCooldown());
        
        float damage = damageData.Damage * takeDamageMult * defence;
        float knockback = damageData.Knockback;
        Vector2 knockbackDir = damageData.Vector;

        health -= damage;   
        UpdateHealthBar();

        timeSinceDamage = 0;

        Knockback(knockback, knockbackDir);

        // Camera shake
        CameraShakeManager.Singleton._CameraShake(intensity, duration, frequency, CameraShakeManager.CurveType.InverseParabola);

        // Damage particles
        transform.GetChild(3).GetChild(2).GetComponent<ParticleSystem>().Play();

        // Damage sound
        SoundManager.Singleton.PlaySound("Player sounds", "PlayerDamage", -1, 0.1f);

        if (!BarrierMain.Singleton.outOfBounds) PlayerVignetteManager.Singleton.DamagePlayerVignette();

        if (cactus != 0 && damageData.Attacker != null) {
            PlayerDealDamage(damageData.Attacker, new DamageData(damage * cactus, cactusKnockback, transform.position - damageData.Attacker.transform.position));
        }
    }

    Color originColor;
    private IEnumerator DamageCooldown()
    {
        originColor = GetComponent<SpriteRenderer>().color;
        GetComponent<SpriteRenderer>().color = Color.white;

        yield return new WaitForSeconds(invincibilityTime);
        damageCooldownCoroutine = null;
        GetComponent<SpriteRenderer>().color = originColor;
    }

    public void GiveCoins(int amount)
    {
        coins += Mathf.FloorToInt(amount * stonks);
    }

    public void UpdateHealthBar()
    {
        healthbar.value = health;
        healthbar.maxValue = maxHealth;

        healthbar.transform.GetChild(2).GetComponent<TextMeshProUGUI>().text = $"{Mathf.Round(health)} / {maxHealth}";
    }
    
    float timeSinceDamage;
    bool naturallyRegening;
    private void PlayerNaturalHealthRegeneration()
    {
        if (health != maxHealth && timeSinceDamage >= damageRegenCooldown + additionalDamageRegenCooldown) {
            if (healCoroutine == null)
            {
                naturallyRegening = true;
                // Start healing
                healCoroutine = StartCoroutine(Heal());
            }
        } else naturallyRegening = false;

        timeSinceDamage += Time.deltaTime;
    }

    Coroutine healCoroutine;
    private IEnumerator Heal()
    {
        yield return new WaitForSeconds(regenTickCooldown);

        //Debug.Log("Healing player");
        HealPlayer(regenValue + additionalRegenValue, false);
        healCoroutine = null;
    }

    public void HealPlayer(float healAmount, bool vignette)
    {
        health += healAmount;
        health = Mathf.Round(health * 10) / 10;
        health = Mathf.Clamp(health, 0, maxHealth);
        UpdateHealthBar();
    }

    public void SetPlayerHealth(float value)
    {
        health = value;
        UpdateHealthBar();
    }

    [HideInInspector] public bool canDash = false;
    bool dashing = false;
    bool lerpGravity = false;
    float gravity = 0;
    IEnumerator Dash()
    {
        rb.gravityScale = 0;
        canDash = false;
        dashing = true;
        Vector2 movementVector = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")).normalized;
        rb.velocity = movementVector * dashForce;
        tr.emitting = true;
        
        yield return new WaitForSeconds(dashDuration);

        rb.gravityScale = _gravity;
        dashing = false;
        rb.velocity = new Vector2(0, 0);
        lerpGravity = true;
        tr.emitting = false;

        yield return new WaitForSeconds(dashCooldown);

        canDash = true;
    }

    private void ApplyFriction()
    {
        Vector2 velocity = rb.velocity;
        if (Mathf.Abs(velocity.x) > 0.1f)
            velocity.x = Mathf.Lerp(velocity.x, 0, Time.deltaTime * _friction);
        else 
            velocity.x = 0;

        rb.velocity = velocity;
    }

    private void Knockback(float Knockback, Vector2 dir)
    {
        if (dir == Vector2.zero) return;
        rb.velocity = dir.normalized * Knockback;
    }

    [HideInInspector] public bool playerOutOfBounds;
    Coroutine oobDamageCoroutine;
    private void PlayerOutOfBounds()
    {
        if (oobDamageCoroutine == null && playerOutOfBounds) oobDamageCoroutine = StartCoroutine(OutOfBoundsDamage());
    }

    private IEnumerator OutOfBoundsDamage()
    {
        yield return new WaitForSeconds(outOfBoundsDamageCooldown);

        if (playerOutOfBounds) DamagePlayer(new DamageData(outOfBoundsDamage, 0, Vector2.zero));

        oobDamageCoroutine = null;
    }

    public void PlayerDealDamage(GameObject target, DamageData damageData)
    {
        float lifeSteal = DataPersistency.player_lifeStealPercentage;
        if (lifeSteal != -1) HealPlayer(damageData.Damage * lifeSteal, true);

        float critMult = UnityEngine.Random.Range(0, 100) < critChance * 100 ? 2 : 1;

        DamageData finalDamage = new DamageData(damageData.Damage * damageMult * critMult, damageData.Knockback, damageData.Vector);

        if (UnityEngine.Random.Range(0f, 1f) < randomBleedChance) PlayerBleedEnemy(target, 5);
        if (UnityEngine.Random.Range(0f, 1f) < randomStunChance) PlayerStunEnemy(target, 1);

        target.SendMessage("DamageEnemy", finalDamage, SendMessageOptions.DontRequireReceiver);
    }

    public void PlayerBleedEnemy(GameObject target, float bleedDamage)
    {
        target.SendMessage("BleedEnemy", bleedDamage,  SendMessageOptions.DontRequireReceiver);
    }

    public void PlayerStunEnemy(GameObject target, float stunTime)
    {
        target.SendMessage("StunEnemy", stunTime,  SendMessageOptions.DontRequireReceiver);
    }

    [HideInInspector] public bool withinChestRange;
    bool itemSelectionMenuOpen;
    void OnTriggerEnter2D(Collider2D collider)
    {
        // Player trigger
        if (collider.gameObject.layer == 15)
        {
            // Chest
            if (collider.gameObject.tag == "Chest")
            {
                PlayerInfoBoard.Singleton.ShowInfoBoard("[E] Open chest");
                withinChestRange = true;

                foreach (ChestSpawnData chestData in TerrainGenerator.Singleton.chunkChestLookup.Values)
                {
                    if (chestData.GlobalPosition == Vector3.zero) continue;
                    if (chestData.GlobalPosition == collider.transform.position) {
                        chestWithinRangeData = chestData;
                        break;
                    }
                }
            }
        }
    }

    void OnTriggerExit2D(Collider2D collider)
    {
        // Player trigger
        if (collider.gameObject.layer == 15)
        {
            // Chest
            if (collider.gameObject.tag == "Chest")
            {
                PlayerInfoBoard.Singleton.HideInfoBoard();
                withinChestRange = false;
                itemSelectionMenuOpen = false;
                ItemSelectionMenuSystem.Singleton.CloseItemSelectionMenu();
            }
        }
    }

    List<float> times;
    EnemySpawnManager enemySpawnManager;
    private void DebugLogs()
    {
        if (enemySpawnManager == null) {
            enemySpawnManager = EnemySpawnManager.Singletone;
            times = new List<float> { 0, 0, 0, 0, 0 };
        }
        times[enemySpawnManager.stage - 1] += Time.deltaTime;
    }

    public void PurchaseSniperScope()
    {
        Debug.Log("Pruchase sniper scope");
        critChance += sniperScopeIncrement;
    }

    public void PurchaseBloodyDagger()
    {
        Debug.Log("Purchase bloody dagger");
        randomBleedChance += bloodyDaggerIncrement;
    }

    public void PurchaseStunPerk()
    {
        Debug.Log("Purchase stun perk");
        randomStunChance += stunPerkIncrement;
    }

    public void PurchaseAirDefence()
    {
        Debug.Log("Purchase air defences");
        airDamageReduction += airDefenceIncrement;
    }

    public void PurchaseCactus()
    {
        Debug.Log("Purchase cactus");
        cactus += cactusIncrement;
    }

    public void PurchaseStonks()
    {
        Debug.Log("Purchase cactus");
        stonks += stonksIncrease;
    }

    [SerializeField] float healthUpgraqeIncr;
    public void PurchaseHealthUpgrade()
    {
        Debug.Log("Purchase health");
        maxHealth += healthUpgraqeIncr;
        UpdateHealthBar();
    }

    [SerializeField] float regenUpgrade = 0.2f;
    public void PurchaseBandaid()
    {
        Debug.Log("Purchase regen");
        regenValue += regenUpgrade;        
    }

    [SerializeField] float lifeStealUpgrade = 0.2f;
    public void PurchaseLifeSteal()
    {
        Debug.Log("Purchase lifesteal");
        DataPersistency.player_lifeStealPercentage = lifeStealUpgrade;
    }

    [SerializeField] float movementSpeedUpgrade = 0.25f;
    public void PurchaseSneaker()
    {
        Debug.Log("Purchase sneaker");
        movementSpeed *= movementSpeedUpgrade + 1;
    }

    [SerializeField] float jumpBoostUpgade = 0.2f;
    public void PurchaseJumpBoost()
    {
        Debug.Log("Purchase jump boost");
        jumpForce += jumpBoostUpgade;
    }

    [SerializeField] int doubleJumpUpgrade = 2;
    public void PurchaseDoubleJump()
    {
        Debug.Log("Purchase double jump");
        maxDoubleJump += doubleJumpUpgrade;
    }
}

public class DamageData 
{
    public float Damage;
    public float Knockback;
    public Vector2 Vector;
    public GameObject Attacker;

    public DamageData(float damage, float knockback, Vector2 vector, GameObject attacker = null)
    {
        Damage = damage;
        Knockback = knockback;
        Vector = vector;
        Attacker = attacker;
    }
}
Editor is loading...
Leave a Comment