Forum begins after the advertisement:


[Part 17] Magnet Pickup

Viewing 10 posts - 1 through 10 (of 10 total)
  • Author
    Posts
  • #14488
    Cam
    Level 22
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::

    Been working on a magnet item cant get it working just yet its seems to be heading in the right direction just dont know yet

    Pickup.cs

    using UnityEngine;
    
    public class Pickup : MonoBehaviour
    {
        public enum PickupType
        {
            None,
            Health,
            Experience,
            Magnet
        }
    
        public float lifespan = 0.5f;
        protected PlayerStats target; 
        protected float speed; 
        Vector2 initialPosition;
        float initialOffset;
        public PickupType type = PickupType.None;
        public float magnetPullForce; // Adjust the pull force as needed
    
        [System.Serializable]
        public struct BobbingAnimation
        {
            public float frequency;
            public Vector2 direction;
        }
        public BobbingAnimation bobbingAnimation = new BobbingAnimation {
            frequency = 2f, direction = new Vector2(0,0.3f)
        };
    
        [Header("Bonuses")]
        public int experience;
        public int health;
    
        protected virtual void Start()
        {
            initialPosition = transform.position;
            initialOffset = Random.Range(0, bobbingAnimation.frequency);
        }
    
        protected virtual void Update()
        {
            if(target)
            {
                Vector2 distance = target.transform.position - transform.position;
                if (distance.sqrMagnitude > speed * speed * Time.deltaTime)
                    transform.position += (Vector3)distance.normalized * speed * Time.deltaTime;
                else
                    Destroy(gameObject);
            }
            else
            {
                transform.position = initialPosition + bobbingAnimation.direction * Mathf.Sin((Time.time + initialOffset) * bobbingAnimation.frequency);
            }
    
            if (type == PickupType.Magnet)
            {
                // Find all experience pickups in the scene
                Pickup[] pickups = FindObjectsOfType<Pickup>();
    
                // Attract each experience pickup towards the player
                foreach (Pickup pickup in pickups)
                {
                    if (pickup.type == PickupType.Experience)
                    {
                        Vector2 direction = (transform.position - pickup.transform.position).normalized;
                        pickup.AttractToPlayer(direction, speed);
                    }
                }
            }
        }
    
        public virtual bool Collect(PlayerStats target, float speed, float lifespan = 0f)
        {
            if (!this.target)
            {
                this.target = target;
                this.speed = speed;
                if (lifespan > 0) this.lifespan = lifespan;
                Destroy(gameObject, Mathf.Max(0.01f, this.lifespan));
                return true;
            }
            return false;
        }
    
        protected virtual void OnDestroy()
        {
            if (!target) return;
            target.IncreaseExperience(experience);
            target.RestoreHealth(health);
        }
    
       public void AttractToPlayer(Vector2 playerPosition, float speed)
        {
            // Calculate the direction from the pickup to the player
            Vector2 direction = (playerPosition - (Vector2)transform.position).normalized;
    
            // Move towards the player's position at the specified speed
            transform.position += (Vector3)direction * speed * Time.deltaTime;
        }
    }
    #14496
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Hi Cam, sorry I missed your topic.

    Don’t recode the Pickup class. Extend it instead, and override the Pickup functions to change its behaviour. That way, you don’t have to risk it affecting your existing pickups:

    public class MagnetPickup : Pickup
    {
        float trackTimer; // How long this pickup has been chasing the player.
        bool inEffect = false; // When the magnet touches the player, set this to true, so the Magnet performs its buff.
    
        [Header("Magnet")]
        public float effectDuration = 2f;
    
        // Here, we remove the Destroy() part of the original Update() function and just set inEffect to true.
        // Then, when the magnet is in effect, we do something else.
        protected override void Update()
        {
            if(target)
            {
                if(inEffect)
                {
                    transform.position = target.position; // Move the pickup to the player's position.
    
                    duration -= Time.deltaTime;
                    if(duration > 0)
                        // Do your magnet effect here.
                    else
                        Destroy(gameObject);
                }
                else
                {
                    // Move it towards the player and check the distance between.
                    Vector2 distance = target.transform.position - transform.position;
                    if (distance.sqrMagnitude > speed * speed * Time.deltaTime)
                        transform.position += (Vector3)distance.normalized * speed * Time.deltaTime;
                    else
                    {
                        // When it becomes active, we hide all its colliders and renderers to "hide" it.
                        inEffect = true;
                        Collider2D[] allColliders = GetComponentsInChildren<Collider2D>();
                        Renderer[] allRenderers = GetComponentsInChildren<Renderer>();
                        foreach(Collider2D c in allColliders) c.enabled = false;
                        foreach(Renderer r in allRenderers) r.enabled = false;
                    }
    
                    // Tracks how long we have been tracking the target.
                    // If it's been doing it for too long, destroy this pickup.
                    trackTimer += Time.deltaTime;
                    if(trackTimer > lifespan) Destroy(gameObject);
                }
    
            }
            else
            {
                // Handle the animation of the object.
                transform.position = initialPosition + bobbingAnimation.direction * Mathf.Sin((Time.time + initialOffset) * bobbingAnimation.frequency);
            }
        }
    
        // We override the old collect function because we don't want magnet pickups to destroy themselves.
        // We will use the trackTimer variable to track how long a MagnetPickup has been tracking an object instead.
        public override bool Collect(PlayerStats target, float speed, float lifespan = 0f)
        {
            if (!this.target)
            {
                this.target = target;
                this.speed = speed;
                if (lifespan > 0) this.lifespan = lifespan;
                return true;
            }
            return false;
        }
    }
    

    I haven’t tested this code yet, but what I’m trying to do is, once it touches the player, I will hide all renderers and colliders it has, then make it follow the player and perform its effect for a certain amount of time, then destroy itself.

    I override the Collect() and Update() functions because I want to modify the pickup’s behaviour. Primarily, besides implementing the magnet functionality, I also want to stop it from destroying itself, as I’m using the Update() function to enact the behaviour, and if the GameObject destroys itself, the script stops as well.

    Hope this helps!

    #14538
    Cam
    Level 22
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::
    <code>using UnityEngine;
    
    public class MagnetPickup : Pickup
    {
        float trackTimer; // How long this pickup has been chasing the player.
        bool inEffect = false; // When the magnet touches the player, set this to true, so the Magnet performs its buff.
        float remainingEffectDuration; // Remaining duration of the magnet effect.
    
        [Header("Magnet")]
        public float effectDuration = 2f;
        public float magnetRange = 90f; // The range within which pickups are collected.
    
        protected override void Update()
        {
            if (target)
            {
                if (inEffect)
                {
                    transform.position = target.transform.position; // Move the pickup to the player's position.
                    remainingEffectDuration -= Time.deltaTime;
    
                }
                else
                {
                    // Move it towards the player and check the distance between.
                    Vector2 distance = target.transform.position - transform.position;
                    if (distance.sqrMagnitude > speed * speed * Time.deltaTime)
                        transform.position += (Vector3)distance.normalized * speed * Time.deltaTime;
                    else
                    {
                        // When it becomes active, disable the pickup GameObject.
                        inEffect = true;
                        remainingEffectDuration = effectDuration;
                        gameObject.SetActive(false);
                    }
    
                    // Tracks how long we have been tracking the target.
                    trackTimer += Time.deltaTime;
    
                }
    
                // Check for nearby pickups and collect them
                Collider2D[] nearbyPickups = Physics2D.OverlapCircleAll(transform.position, magnetRange);
                foreach (Collider2D pickupCollider in nearbyPickups)
                {
                    Pickup pickup = pickupCollider.GetComponent<Pickup>();
                    if (pickup)
                    {
                        pickup.Collect(target, speed, lifespan);
                    }
                }
    
            }
            else
            {
                // Handle the animation of the object.
                transform.position = initialPosition + bobbingAnimation.direction * Mathf.Sin((Time.time + initialOffset) * bobbingAnimation.frequency);
            }
        }
    
        public override bool Collect(PlayerStats target, float speed, float lifespan = 0f)
        {
            if (!this.target)
            {
                this.target = target;
                this.speed = speed;
                if (lifespan > 0) this.lifespan = lifespan;
                return true;
            }
            return false;
        }
    }
    </code>

    this seems to be working now the next issue is when i collect a few xp gems, i only get one level up screen. eg i cooloected the magnet got like 6 gems had one level up screen but i was level 4 after collecting the gems

    #14541
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Cam, do your gems give experience once you touch them? Or do they have to fly over to you first?

    #14548
    Cam
    Level 22
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::

    they fly towards me 1st

    #14549
    Cam
    Level 22
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::

    You can recreate this by spawning a few gems in the same spot and collecting them

    #14555
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Update 11 May 2024: Edited the code to reflect the changes highlighted by Cam below.

    Hi Cam, thanks for this. It’s a great bug you spotted. Our current experience system in PlayerStats isn’t able to support multiple experience increases in a single frame, because they will all turn on the level-up screen together (you can’t turn on the level-up screen more than once).

    I fixed this by limiting calls to IncreaseExperience() to 1 per frame. If you call it more than once, it will keep a backlog. Here’s how it looks like (changes in green):

    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using TMPro;
    
    public class PlayerStats : MonoBehaviour
    {
    
        CharacterData characterData;
        public CharacterData.Stats baseStats;
        [SerializeField] CharacterData.Stats actualStats;
    
        public CharacterData.Stats Stats
        {
            get { return actualStats;  }
            set { 
                actualStats = value;
            }
        }
    
        float health;
    
        #region Current Stats Properties
        public float CurrentHealth
        {
    
            get { return health; }
    
            // If we try and set the current health, the UI interface
            // on the pause screen will also be updated.
            set
            {
                //Check if the value has changed
    
                if (health != value)
                {
                    health = value;
                    UpdateHealthBar();
                }
            }
        }
        #endregion
    
        [Header("Visuals")]
        public ParticleSystem damageEffect; // If damage is dealt.
        public ParticleSystem blockedEffect; // If armor completely blocks damage.
    
        //Experience and level of the player
        [Header("Experience/Level")]
        public int experience = 0;
        public int level = 1;
        public int experienceCap;
        bool hasIncreasedExperience = false;
        Queue<int> experienceBacklog = new Queue<int>();
    
        //Class for defining a level range and the corresponding experience cap increase for that range
        [System.Serializable]
        public class LevelRange
        {
            public int startLevel;
            public int endLevel;
            public int experienceCapIncrease;
        }
    
        //I-Frames
        [Header("I-Frames")]
        public float invincibilityDuration;
        float invincibilityTimer;
        bool isInvincible;
    
        public List<LevelRange> levelRanges;
    
    
        PlayerInventory inventory;
        PlayerCollector collector;
        public int weaponIndex;
        public int passiveItemIndex;
    
        [Header("UI")]
        public Image healthBar;
        public Image expBar;
        public TMP_Text levelText;
    
        void Awake()
        {
            characterData = CharacterSelector.GetData();
            if(CharacterSelector.instance) 
                CharacterSelector.instance.DestroySingleton();
    
            inventory = GetComponent<PlayerInventory>();
            collector = GetComponentInChildren<PlayerCollector>();
    
            //Assign the variables
            baseStats = actualStats = characterData.stats;
            collector.SetRadius(actualStats.magnet);
            health = actualStats.maxHealth;
        }
    
        void Start()
        {
            //Spawn the starting weapon
            inventory.Add(characterData.StartingWeapon);
    
            //Initialize the experience cap as the first experience cap increase
            experienceCap = levelRanges[0].experienceCapIncrease;
    
            GameManager.instance.AssignChosenCharacterUI(characterData);
    
            UpdateHealthBar();
            UpdateExpBar();
            UpdateLevelText();
        }
    
        void Update()
        {
            hasIncreasedExperience = false;
            if(experienceBacklog.Count > 0 && GameManager.instance.currentState != GameManager.GameState.LevelUp) {
                IncreaseExperience(experienceBacklog.Dequeue());
            }
    
    
            if (invincibilityTimer > 0)
            {
                invincibilityTimer -= Time.deltaTime;
            }
            //If the invincibility timer has reached 0, set the invincibility flag to false
            else if (isInvincible)
            {
                isInvincible = false;
            }
    
            Recover();
        }
    
        public void RecalculateStats()
        {
            actualStats = baseStats;
            foreach (PlayerInventory.Slot s in inventory.passiveSlots)
            {
                Passive p = s.item as Passive;
                if (p)
                {
                    actualStats += p.GetBoosts();
                }
            }
    
            // Update the PlayerCollector's radius.
            collector.SetRadius(actualStats.magnet);
        }
    
        public void IncreaseExperience(int amount)
        {
            if(hasIncreasedExperience)
            {
                experienceBacklog.Enqueue(amount);
                return;
            }
    
            experience += amount;
            LevelUpChecker();
            UpdateExpBar();
            hasIncreasedExperience = true;
        }
    
        void LevelUpChecker()
        {
            if (experience >= experienceCap)
            {
                //Level up the player and reduce their experience by the experience cap
                level++;
                experience -= experienceCap;
    
                //Find the experience cap increase for the current level range
                int experienceCapIncrease = 0;
                foreach (LevelRange range in levelRanges)
                {
                    if (level >= range.startLevel && level <= range.endLevel)
                    {
                        experienceCapIncrease = range.experienceCapIncrease;
                        break;
                    }
                }
                experienceCap += experienceCapIncrease;
    
                UpdateLevelText();
    
                GameManager.instance.StartLevelUp();
            }
        }
    
        void UpdateExpBar()
        {
            // Update exp bar fill amount
            expBar.fillAmount = (float)experience / experienceCap;
        }
    
        void UpdateLevelText()
        {
            // Update level text
            levelText.text = "LV " + level.ToString();
        }
    
        public void TakeDamage(float dmg)
        {
            //If the player is not currently invincible, reduce health and start invincibility
            if (!isInvincible)
            {
                // Take armor into account before dealing the damage.
                dmg -= actualStats.armor;
    
                if (dmg > 0)
                {
                    // Deal the damage.
                    CurrentHealth -= dmg;
    
                    // If there is a damage effect assigned, play it.
                    if (damageEffect) Destroy(Instantiate(damageEffect, transform.position, Quaternion.identity), 5f);
    
                    if (CurrentHealth <= 0)
                    {
                        Kill();
                    }
                }
                else
                {
                    // If there is a blocked effect assigned, play it.
                    if (blockedEffect) Destroy(Instantiate(blockedEffect, transform.position, Quaternion.identity), 5f);
                }
    
                invincibilityTimer = invincibilityDuration;
                isInvincible = true;
            }
        }
    
        void UpdateHealthBar()
        {
            //Update the health bar
            healthBar.fillAmount = CurrentHealth / actualStats.maxHealth;
        }
    
        public void Kill()
        {
            if (!GameManager.instance.isGameOver)
            {
                GameManager.instance.AssignLevelReachedUI(level);
                //GameManager.instance.AssignChosenWeaponsAndPassiveItemsUI(inventory.weaponSlots, inventory.passiveSlots);
                GameManager.instance.GameOver();
            }
        }
    
        public void RestoreHealth(float amount)
        {
            // Only heal the player if their current health is less than their maximum health
            if (CurrentHealth < actualStats.maxHealth)
            {
                CurrentHealth += amount;
    
                // Make sure the player's health doesn't exceed their maximum health
                if (CurrentHealth > actualStats.maxHealth)
                {
                    CurrentHealth = actualStats.maxHealth;
                }
            }
        }
    
        void Recover()
        {
            if (CurrentHealth < actualStats.maxHealth)
            {
                CurrentHealth += Stats.recovery * Time.deltaTime;
    
                // Make sure the player's health doesn't exceed their maximum health
                if (CurrentHealth > actualStats.maxHealth)
                {
                    CurrentHealth = actualStats.maxHealth;
                }
            }
        }
    }

    I found another bug though: If an experience gem gives you enough experience to increase your level by more than 1, this still registers only as 1 level up (even though the level up bar goes way up). I’ll be deploying a fix for this as well.

    P.S. Added another bug spotter badge on your profile as thanks for pointing this out!

    #14558
    Cam
    Level 22
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::

    i had to fix that code some more to get it to work but now its seems to work well

    <code>using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using TMPro;
    
    public class PlayerStats : MonoBehaviour
    {
    
        CharacterData characterData;
        public CharacterData.Stats baseStats;
        [SerializeField] CharacterData.Stats actualStats;
    
        public CharacterData.Stats Stats
        {
            get { return actualStats;  }
            set { 
                actualStats = value;
            }
        }
    
        float health;
    
        #region Current Stats Properties
        public float CurrentHealth
        {
    
            get { return health; }
    
            // If we try and set the current health, the UI interface
            // on the pause screen will also be updated.
            set
            {
                //Check if the value has changed
    
                if (health != value)
                {
                    health = value;
                    UpdateHealthBar();
                }
            }
        }
        #endregion
    
        [Header("Visuals")]
        public ParticleSystem damageEffect; // If damage is dealt.
        public ParticleSystem blockedEffect; // If armor completely blocks damage.
    
        //Experience and level of the player
        [Header("Experience/Level")]
        public int experience = 0;
        public int level = 1;
        public int experienceCap;
        bool hasIncreasedExperience = false;
        Queue<int> experienceBacklog = new Queue<int>();
    
        //Class for defining a level range and the corresponding experience cap increase for that range
        [System.Serializable]
        public class LevelRange
        {
            public int startLevel;
            public int endLevel;
            public int experienceCapIncrease;
        }
    
        //I-Frames
        [Header("I-Frames")]
        public float invincibilityDuration;
        float invincibilityTimer;
        bool isInvincible;
    
        public List<LevelRange> levelRanges;
    
        PlayerInventory inventory;
        PlayerCollector collector;
        public int weaponIndex;
        public int passiveItemIndex;
    
        [Header("UI")]
        public Image healthBar;
        public Image expBar;
        public TMP_Text levelText;
    
        void Awake()
        {
            characterData = CharacterSelector.GetData();
            if(CharacterSelector.instance) 
                CharacterSelector.instance.DestroySingleton();
    
            inventory = GetComponent<PlayerInventory>(); // Assuming PlayerInventory is the type you want to get.
            collector = GetComponentInChildren<PlayerCollector>(); // Assuming PlayerCollector is the type you want to get.
    
    
    
            //Assign the variables
            baseStats = actualStats = characterData.stats;
            collector.SetRadius(actualStats.magnet);
            health = actualStats.maxHealth;
        }
    
        void Start()
        {
            //Spawn the starting weapon
            inventory.Add(characterData.StartingWeapon);
    
            //Initialize the experience cap as the first experience cap increase
            experienceCap = levelRanges[0].experienceCapIncrease;
    
            GameManager.instance.AssignChosenCharacterUI(characterData);
    
            UpdateHealthBar();
            UpdateExpBar();
            UpdateLevelText();
        }
    
        void Update()
        {
            hasIncreasedExperience = false;
            if(experienceBacklog.Count > 0 && GameManager.instance.currentState != GameManager.GameState.LevelUp) {
                IncreaseExperience(experienceBacklog.Dequeue());
            }
    
    
            if (invincibilityTimer > 0)
            {
                invincibilityTimer -= Time.deltaTime;
            }
            //If the invincibility timer has reached 0, set the invincibility flag to false
            else if (isInvincible)
            {
                isInvincible = false;
            }
    
            Recover();
        }
    
        public void RecalculateStats()
        {
            actualStats = baseStats;
            foreach (PlayerInventory.Slot s in inventory.passiveSlots)
            {
                Passive p = s.item as Passive;
                if (p)
                {
                    actualStats += p.GetBoosts();
                }
            }
    
            // Update the PlayerCollector's radius.
            collector.SetRadius(actualStats.magnet);
        }
    
        public void IncreaseExperience(int amount)
        {
            if(hasIncreasedExperience)
            {
                experienceBacklog.Enqueue(amount);
                return;
            }
    
            experience += amount;
            LevelUpChecker();
            UpdateExpBar();
            hasIncreasedExperience = true;
        }
    
        void LevelUpChecker()
        {
            if (experience >= experienceCap)
            {
                //Level up the player and reduce their experience by the experience cap
                level++;
                experience -= experienceCap;
    
                //Find the experience cap increase for the current level range
                int experienceCapIncrease = 0;
                foreach (LevelRange range in levelRanges)
                {
                    if (level >= range.startLevel && level <= range.endLevel)
                    {
                        experienceCapIncrease = range.experienceCapIncrease;
                        break;
                    }
                }
                experienceCap += experienceCapIncrease;
    
                UpdateLevelText();
    
                GameManager.instance.StartLevelUp();
            }
        }
    
        void UpdateExpBar()
        {
            // Update exp bar fill amount
            expBar.fillAmount = (float)experience / experienceCap;
        }
    
        void UpdateLevelText()
        {
            // Update level text
            levelText.text = "LV " + level.ToString();
        }
    
        public void TakeDamage(float dmg)
        {
            //If the player is not currently invincible, reduce health and start invincibility
            if (!isInvincible)
            {
                // Take armor into account before dealing the damage.
                dmg -= actualStats.armor;
    
                if (dmg > 0)
                {
                    // Deal the damage.
                    CurrentHealth -= dmg;
    
                    // If there is a damage effect assigned, play it.
                    if (damageEffect) Destroy(Instantiate(damageEffect, transform.position, Quaternion.identity), 5f);
    
                    if (CurrentHealth <= 0)
                    {
                        Kill();
                    }
                }
                else
                {
                    // If there is a blocked effect assigned, play it.
                    if (blockedEffect) Destroy(Instantiate(blockedEffect, transform.position, Quaternion.identity), 5f);
                }
    
                invincibilityTimer = invincibilityDuration;
                isInvincible = true;
            }
        }
    
        void UpdateHealthBar()
        {
            //Update the health bar
            healthBar.fillAmount = CurrentHealth / actualStats.maxHealth;
        }
    
        public void Kill()
        {
            if (!GameManager.instance.isGameOver)
            {
                GameManager.instance.AssignLevelReachedUI(level);
                //GameManager.instance.AssignChosenWeaponsAndPassiveItemsUI(inventory.weaponSlots, inventory.passiveSlots);
                GameManager.instance.GameOver();
            }
        }
    
        public void RestoreHealth(float amount)
        {
            // Only heal the player if their current health is less than their maximum health
            if (CurrentHealth < actualStats.maxHealth)
            {
                CurrentHealth += amount;
    
                // Make sure the player's health doesn't exceed their maximum health
                if (CurrentHealth > actualStats.maxHealth)
                {
                    CurrentHealth = actualStats.maxHealth;
                }
            }
        }
    
        void Recover()
        {
            if (CurrentHealth < actualStats.maxHealth)
            {
                CurrentHealth += Stats.recovery * Time.deltaTime;
    
                // Make sure the player's health doesn't exceed their maximum health
                if (CurrentHealth > actualStats.maxHealth)
                {
                    CurrentHealth = actualStats.maxHealth;
                }
            }
        }
    }
    </code>

    sorry dont know how to use the green text the changes was

    <code>
    inventory = GetComponent<PlayerInventory>(); 
    collector = GetComponentInChildren<PlayerCollector>();
    </code>
    <code>
    Queue<int> experienceBacklog = new Queue<int>();
    </code>
    <code>
    public List<LevelRange> levelRanges;
    </code>
    #14563
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    No problem Cam. Thanks for helping to document this!

    To green highlight text, you’ll can use the <mark class="green"> and </mark> tags.

    #14635
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Cam, FYI, I’ve added a new section in the Part 19 article addressing the experience bug you mentioned: https://blog.terresquall.com/2024/04/creating-a-rogue-like-vampire-survivors-part-19/#bugfixes-for-experience-gain

    The fixes in there are simpler than the code I’ve given you. Do take a look if you’re interested.

    There is also a fix for when you give too much experience using PlayerStats.IncreaseExperience(), such that the player is able to gain multiple levels from the experience given. In such cases, you will only gain a single level:

    [video src="https://blog.terresquall.com/wp-content/uploads/2024/04/vampire-survivors-too-much-experience-given.webm" /]

Viewing 10 posts - 1 through 10 (of 10 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: