Player.cs

 

#region Using Statements
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Project_Daedalus.Game_World.Characters.PlayerAttacks;
using Project_Daedalus.Game_World.Classes.Characters.PlayerAbilities;
using ParadigmEngine.TileEngine;
using ProtoplasmEngine;
using FarseerGames.FarseerPhysics;
using FarseerGames.FarseerPhysics.Dynamics;
using FarseerGames.FarseerPhysics.Dynamics.Joints;
using FarseerGames.FarseerPhysics.Factories;
using FarseerGames.FarseerPhysics.Collisions;
#endregion

namespace Project_Daedalus.Game_World.Characters
{
    /// <summary>
    /// The player character
    /// </summary>
    public class Player : Character
    {
        /// <summary>
        /// Event handling portal collisions
        /// </summary>
        public event PortalCollisionEvent PortalCollision;

        /// <summary>
        /// Event handing player death
        /// </summary>
        public event PlayerDeathEvent PlayerDeath;

        #region Fields
        /// <summary>
        /// Friction while on wall
        /// </summary>
        private const float WallFrictionCoefficiant = .09f;

        /// <summary>
        /// Friction while on ground
        /// </summary>
        private const float GroundFrictionCoefficiant = 3f;

        /// <summary>
        /// Max distance for rope shot
        /// TODO:  Integrate this into RopeShot class
        /// </summary>
        private const float RopeDist = 400f;

        // attack / weapon states
        /// <summary>
        /// Did player just attack
        /// </summary>
        private bool justAttacked;

        /// <summary>
        /// Did we just switch weapons
        /// </summary>
        private bool justSwitched;

        /// <summary>
        /// Current weapon index
        /// </summary>
        private int currentWeaponIndex = 0;

        // respawn info
        /// <summary>
        /// Respawn timer
        /// </summary>
        private float deathWaitTimer = 0.0f;

        /// <summary>
        /// Time between respawns
        /// </summary>
        private const float MaxDeathWaitTime = 3.0f;

        /// <summary>
        /// Is player invincible
        /// </summary>
        private bool isInvincible = false;

        /// <summary>
        /// Invincible timer
        /// </summary>
        private float invincibleTimer = 0.0f;

        /// <summary>
        /// How long does the player remain invincible after damage is received
        /// (in seconds)
        /// </summary>
        private const float MaxInvincibleTime = 2.0f;

        /// <summary>
        /// Blink timer (used for invincibility)
        /// </summary>
        private float blinkTimer = 0.0f;

        /// <summary>
        /// How long between sprite blinks when invincible (in seconds)
        /// </summary>
        private const float MaxBlinkTime = 0.15f;

        // attack classes and variables
        /// <summary>
        /// Current attack
        /// </summary>
        private Attack currentAttack;

        /// <summary>
        /// Container holding all available attacks
        /// </summary>
        private List<Attack> availableAttacks;

        /// <summary>
        /// Normal slash attack
        /// </summary>
        private NormalAttack normalAttack;

        /// <summary>
        /// Flamethrower
        /// </summary>
        private FlameThrowerAttack flameThrower;

        /// <summary>
        /// Ice shield
        /// </summary>
        private IceShieldAttack iceShield;

        /// <summary>
        /// Time freeze
        /// </summary>
        private TimeFreezeAttack timeFreeze;

        /// <summary>
        /// Grappling hook
        /// </summary>
        private GrapplingHook grapplingHook;

        /// <summary>
        /// Are player currently stopping time
        /// </summary>
        private bool isStoppingTime = false;

        /// <summary>
        /// Current mana amount
        /// </summary>
        private float mana;

        /// <summary>
        /// Maximum mana amount
        /// </summary>
        private float maxMana = 100;

        /// <summary>
        /// Last geometry we collided with
        /// </summary>
        private Geom lastCollided;
        #endregion

        #region Attributes
        /// <summary>
        /// Did player just attack
        /// </summary>
        public bool JustAttacked
        {
            get { return justAttacked; }
        }

        /// <summary>
        /// Is player invincible
        /// </summary>
        public bool IsInvincible
        {
            get { return isInvincible; }
        }

        /// <summary>
        /// Current attack
        /// </summary>
        public Attack CurrentAttack
        {
            get { return currentAttack; }
        }

        /// <summary>
        /// Is player currently stopping time
        /// </summary>
        public bool IsStoppingTime
        {
            get { return isStoppingTime; }
            set { isStoppingTime = value; }
        }

        /// <summary>
        /// Current mana amount
        /// </summary>
        public float Mana
        {
            get { return mana; }
            set { mana = value; }
        }

        /// <summary>
        /// Maximum mana amount
        /// </summary>
        public float MaxMana
        {
            get { return maxMana; }
            set { maxMana = value; }
        }

        #endregion

        #region Constructors
        /// <summary>
        /// Constructors a new player.
        /// </summary>
        public Player(GameWorldArea gameWorld, Map map)
        {
            LoadContent("Player");

            this.gameWorld = gameWorld;
            this.map = map;
            maxHitPoints = 100;
            flip = SpriteEffects.None;

            canWallJump = true;

            // initialize attacks and set current
            normalAttack = new NormalAttack(this);
            flameThrower = new FlameThrowerAttack(this);
            iceShield = new IceShieldAttack(this);
            timeFreeze = new TimeFreezeAttack(this);
            grapplingHook = new GrapplingHook(this);

            availableAttacks = new List<Attack>();

            availableAttacks.Add(normalAttack);
            availableAttacks.Add(flameThrower);
            availableAttacks.Add(iceShield);
            availableAttacks.Add(timeFreeze);

            currentAttack = normalAttack;

            Reset();

        }
        #endregion

        #region Public Overridden Methods
        /// <summary>
        /// Load all content
        /// </summary>
        /// <param name="entity"></param>
        public override void LoadContent(string entity)
        {
            // Load animated textures.
            idleAnimation = new Animation(GameWorldComponent.StaticContent.Load<Texture2D>("Sprites/Player/Idle"), 0.15f, 6, true);
            runAnimation = new Animation(GameWorldComponent.StaticContent.Load<Texture2D>("Sprites/Player/Run"), 0.1f, 11, true);
            jumpAnimation = new Animation(GameWorldComponent.StaticContent.Load<Texture2D>("Sprites/Player/Jump"), 0.1f, 12, false);
            fallAnimation = new Animation(GameWorldComponent.StaticContent.Load<Texture2D>("Sprites/Player/Fall"), 0.1f, 4, true);
            wallSlideAnimation = new Animation(GameWorldComponent.StaticContent.Load<Texture2D>("Sprites/Player/Run"), 0.1f, 11, true);
            climbUpAnimation = new Animation(GameWorldComponent.StaticContent.Load<Texture2D>("Sprites/Player/Climb_Up"), 2f, 6, true);
            climbDownAnimation = new Animation(GameWorldComponent.StaticContent.Load<Texture2D>("Sprites/Player/Climb_Down"), 2f, 6, true);

            // set sound cue names
            hitSound = "Player_Hit";
            jumpSound = "Player_Jump";
            killedSound = "Player_Death";

            // Calculate bounds within texture size.
            int width = (int)(idleAnimation.FrameWidth * 0.6f);
            int left = (idleAnimation.FrameWidth - width) / 2;
            int height = (int)(idleAnimation.FrameHeight * 0.7f);
            int top = idleAnimation.FrameHeight - height;
            localBounds = new Rectangle(left, top, width, height);

            // create body and geometry
            bodyOffset = (localBounds.Height / 2);
            body = BodyFactory.Instance.CreateEllipseBody(GameWorldComponent.PhysicsSimulator, width / 2, height / 2, 0.1f);
            body.MomentOfInertia = float.MaxValue;

            geom = GeomFactory.Instance.CreateEllipseGeom(GameWorldComponent.PhysicsSimulator, body, width / 2, height / 2, 200);
            geom.FrictionCoefficient = 3f;
            geom.RestitutionCoefficient = 0f;

            // set tags
            List<string> tags = new List<string>();
            tags.Add("Player");
            geom.Tag = tags as object;

            geom.OnCollision += OnCollision;
            geom.CollisionCategories = CollisionCategory.Player;
            geom.CollidesWith = CollisionCategory.All & ~CollisionCategory.Player & ~CollisionCategory.NPC & CollisionCategory.FrozenNPC;
        }

        /// <summary>
        /// Collision event
        /// </summary>
        private bool OnCollision(Geom geometry1, Geom geometry2, ContactList contactList)
        {
            lastCollided = geometry2;

            List<String> tags = geometry2.Tag as List<string>;

            try
            {
                if (tags[0] == "World")
                {
                    try
                    {
                        // check for platform collisions
                        if (tags[1] == "Platform")
                        {
                            foreach (Contact contact in contactList)
                            {
                                // if the normal is <= 0, we know we are entering the collision from
                                // below, and we can cancel the collision
                                if (contact.Normal.Y <= 0)
                                    return false;
                            }
                        }

                        if (isOnLadder)
                        {
                            // check for ladder top collisions
                            if (tags[1] == "Ladder Top")
                            {
                                return false;
                            }
                        }
                    }
                    catch (System.Exception)
                    {

                    }
                }
            }
            catch (NullReferenceException)
            {

            }

            return true;
        }

        /// <summary>
        /// Handles input, performs physics, and animates the player sprite.
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            if (IsAlive)
            {
                float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

                // wait for invincibility to end, then toggle the flag
                if (invincibleTimer <= 0.0f)
                {
                    blinkTimer = 0.0f;
                    isInvincible = false;
                }
                else
                    invincibleTimer = Math.Max(0.0f, invincibleTimer - elapsed);

                if (isInvincible)
                    blinkTimer = Math.Max(0.0f, blinkTimer - elapsed);

                // do not change direction during knockback
                if (!isKnockedBack)
                {
                    // update direction based on velocity
                    if (velocity.X > 0)
                        direction = FaceDirection.Right;

                    if (velocity.X < 0)
                        direction = FaceDirection.Left;
                }

                HandlePortalCollision(gameTime);

                grapplingHook.Update(gameTime);
            }
            else
            {
                // respawn if dead
                Respawn(gameTime);
            }

            // update current attack
            currentAttack.Update(gameTime);

            base.Update(gameTime);
        }

        /// <summary>
        /// Reset the player
        /// </summary>
        public override void Reset()
        {
            hitPoints = 100;
            mana = 100;
            blinkTimer = 0.0f;
            invincibleTimer = 0.0f;
            isInvincible = false;
            direction = FaceDirection.Right;

            body.Position = new Vector2(spawnPoint.X, spawnPoint.Y - bodyOffset);
            base.Reset();
        }

        /// <summary>
        /// Draw player and associated attacks
        /// </summary>
        /// <param name="gameTime"></param>
        /// <param name="spriteBatch"></param>
        public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            currentAttack.Draw(gameTime, spriteBatch);

            if (isInvincible)
            {
                // if we are in the middle of blinking, advance the frame but do not draw
                // otherwise, draw

                if (blinkTimer <= 0)
                {
                    base.Draw(gameTime, spriteBatch);
                    blinkTimer = MaxBlinkTime;
                }
                else
                    AdvanceAnimationFrame(gameTime, spriteBatch);
            }

            if (!isInvincible)
            {
                grapplingHook.Draw(spriteBatch);

                // we use a specialized version of draw here for ladders
                if (!isOnLadder)
                    base.Draw(gameTime, spriteBatch);
                else
                    sprite.Draw(gameTime, spriteBatch, Position, SpriteEffects.None, 1f, Math.Abs(velocity.Y) / 1000f, false);
            }
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Check for and handle collisions with map portals
        /// </summary>
        private void HandlePortalCollision(GameTime gameTime)
        {
            // check for portal collisions
            int mapPortalCollision = map.GetPortalCollision(this.BoundingRectangle);

            // if we collided, get the portal data
            if (mapPortalCollision != -1)
            {
                PortalInfo fromPortal = map.PortalList[mapPortalCollision];
                Vector2 prevPosition = body.Position;

                if (PortalCollision != null)
                    PortalCollision(this, new PortalCollisionEventArgs(fromPortal, prevPosition));

                movement = Vector2.Zero;
            }
        }

        /// <summary>
        /// Respawn the player
        /// </summary>
        private void Respawn(GameTime gameTime)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            // if we need to wait..
            if (deathWaitTimer > 0)
            {
                deathWaitTimer = Math.Max(0.0f, deathWaitTimer - elapsed);
                // Wait for some amount of time.

                if (deathWaitTimer <= 0.0f)
                {
                    if (PlayerDeath != null)
                        PlayerDeath(this);

                    // reset player
                    Reset();
                }
            }
        }

        #endregion

        #region Public Methods
        /// <summary>
        /// Change avatar location based on map change
        /// </summary>
        /// <param name="map">The new map</param>
        /// <param name="position">The new position</param>
        /// <param name="direction">The new direction</param>
        public void MapChange(Map map, Vector2 position, FaceDirection direction)
        {
            this.map = map;
            this.body.Position = position;
            this.direction = direction;

            this.spawnPoint = position;
        }

        /// <summary>
        /// Start a jump
        /// </summary>
        public void Jump(bool unlockJump)
        {
            // if jumping is not currently locked
            if (!unlockJump)
            {
                // if on a ladder, release
                if (isOnLadder)
                    isOnLadder = false;
                else
                {
                    jumpLocked = true;
                    isJumping = true;
                }
            }

            // if we are unlocking the jump
            if (unlockJump)
            {
                if (isOnGround || isOnWall)
                {
                    isJumping = false;
                    jumpLocked = false;
                }
            }
        }

        /// <summary>
        /// Check if we are colliding with a ladder
        /// </summary>
        public void CheckForLadder(bool checkBelow)
        {
            // do not look for ladders below if we are in the air and not on a ladder
            if (checkBelow)
                if ((!isOnLadder) && (inTheAir))
                    return;

            foreach (Entity entity in gameWorld.EntityList)
            {
                Ladder ladder = entity as Ladder;

                // if the cast was successful, check for collision
                if (ladder != null)
                {
                    // for normal checking, we will check all 3 points
                    // if we only checking below, the bottom point is all that matters
                    Point top = new Point(BoundingRectangle.Center.X, BoundingRectangle.Top);
                    Point center = BoundingRectangle.Center;
                    Point bottom;

                    // check further below if we are going down
                    if (checkBelow)
                        bottom = new Point(BoundingRectangle.Center.X, BoundingRectangle.Bottom + 10);
                    else
                        bottom = new Point(BoundingRectangle.Center.X, BoundingRectangle.Bottom);

                    bool ladderCheck = ((ladder.BoundingRectangle.Contains(top)) ||
                                        (ladder.BoundingRectangle.Contains(center)) ||
                                        (ladder.BoundingRectangle.Contains(bottom)));

                    // if any of those 3 points pass a collision test, we are on a ladder
                    if (ladderCheck)
                    {
                        // zero out velocity
                        if (!isOnLadder)
                            body.LinearVelocity = Vector2.Zero;

                        // set position
                         body.Position = new Vector2((float)ladder.BoundingRectangle.Center.X, body.Position.Y);

                        // we are on ladder
                        isOnLadder = true;

                        // break if we hit a ladder
                        return;
                    }
                }
            }

            // we aren't on a ladder
            isOnLadder = false;
        }

        /// <summary>
        /// Switch the current weapon
        /// TODO:  Adjust logic to switch in certain direction when 3rd Weapon is implemented
        /// </summary>
        public void SwitchWeapon(GameTime gameTime, bool releaseSwitch)
        {
            if (!releaseSwitch) // if this is a new switch
            {
                isAttacking = false;

                // if we didn't just switch our weapon
                if (!justSwitched)
                {
                    if (currentWeaponIndex == availableAttacks.Count - 1)
                        currentWeaponIndex = 0;
                    else
                        currentWeaponIndex++;

                    currentAttack = availableAttacks[currentWeaponIndex];

                    // DEBUG: Projective test
                    //AddProjectile(new SmallFireBall(this, new Vector2(1.3f * (float)direction, 0f), new Vector2((float)BoundingRectangle.Center.X, (float)BoundingRectangle.Center.Y)));

                }
                justSwitched = true;
            }
            else // release the switch flag
            {
                justSwitched = false;
            }
        }

        /// <summary>
        /// Attack
        /// TODO: Logic to separate single fire and charged shots
        /// </summary>
        public void Attack(GameTime gameTime, bool releaseAttack)
        {
            if (!releaseAttack) // if this is a new attack
            {
                if (currentAttack.IsSingleFire)
                {
                    isAttacking = true;

                    // if we didn't just attack
                    if (!justAttacked)
                    {
                        currentAttack.DoAttack(gameTime);
                        justAttacked = true;

                        if (currentAttack.LocksMovement)
                            runLocked = true;
                    }
                }
                else
                {
                    isAttacking = true;
                    currentAttack.DoAttack(gameTime);
                    justAttacked = true;
                }
                base.Attack(gameTime);
            }
            else // we are finishing an attack
            {
                isAttacking = false;
                justAttacked = false;
            }
        }

        /// <summary>
        /// Fire ropeshot (grappling hook)
        /// </summary>
        public void RopeShot(bool fireRope)
        {
            if (fireRope)
                grapplingHook.Fire();
            else
                grapplingHook.Disconnect();
        }

        /// <summary>
        /// Do damage to character
        /// </summary>
        public override void TakeDamage(GameTime gameTime, float damageDealt)
        {
            // only take damage if not invincible
            if (!isInvincible)
            {
                // stop jumping in preparation for knockback
                isJumping = false;
                wasJumping = false;

                // perform knockback

                if (direction == FaceDirection.Left)
                    knockbackDirection = FaceDirection.Right;
                else
                    knockbackDirection = FaceDirection.Left;

                Knockback();

                // set invincibility info
                invincibleTimer = MaxInvincibleTime;
                isInvincible = true;

                // process damage
                base.TakeDamage(gameTime, damageDealt);
            }
        }

        /// <summary>
        /// Take damage from a damage source
        /// </summary>
        /// <param name="gameTime"></param>
        /// <param name="damageDealt">Amount of damage</param>
        /// <param name="enemyDirection">Direction damage came from</param>
        public override void TakeDamage(GameTime gameTime, Entity entity, float damageDealt, FaceDirection enemyDirection)
        {
            // only take damage if not invincible
            if (!isInvincible)
            {

                // stop jumping in preparation for knockback
                isJumping = false;
                wasJumping = false;

                // perform knockback

                if (!isOnLadder)
                {
                    // if enemy and player are facing same direction but enemy is not moving, or enemey
                    // and player are moving in same direction reverse knockback direction
                    if (direction == enemyDirection)
                    {
                        if ((entity.Velocity.X > 0) && (Velocity.X > 0) || (entity.Velocity.X < 0) && (Velocity.X < 0) || (entity.Velocity.X == 0))
                        {
                            if (enemyDirection == FaceDirection.Right)
                                enemyDirection = FaceDirection.Left;
                            else
                                enemyDirection = FaceDirection.Right;
                        }
                    }
                }

                // release ladder if we are on one
                isOnLadder = false;

                Knockback(enemyDirection);

                // set invincibility info
                invincibleTimer = MaxInvincibleTime;
                isInvincible = true;

                // process damage
                base.TakeDamage(gameTime, entity, damageDealt, enemyDirection);
            }
        }

        /// <summary>
        /// Kill the player
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Die(GameTime gameTime)
        {
            if (IsAlive)
            {
                // play death sound
                SoundManager.PlaySound("Player_Death", 1f);

                // create explosion at the new position
                GameWorldComponent.GameParticleManager.MakeExplosion(body.Position, 0.5f);
                deathWaitTimer = MaxDeathWaitTime;

                isInvincible = false;

                // end attack
                Attack(gameTime, true);
                isAttacking = false;
                justAttacked = false;

                // release ropeshot
                RopeShot(false);

                // fade out
                // game.Fade();
            }
            base.Die(gameTime);
        }
        #endregion
    }
}