GrenadeSoldier.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 ParadigmEngine.TileEngine;
using ProtoplasmEngine;
using FarseerGames.FarseerPhysics;
using FarseerGames.FarseerPhysics.Dynamics;
using FarseerGames.FarseerPhysics.Factories;
using FarseerGames.FarseerPhysics.Collisions;
#endregion

namespace Project_Daedalus.Game_World.Characters
{
    /// <summary>
    /// Grenade soldier
    /// </summary>
    public class GrenadeSoldier : Enemy
    {
        #region Fields
        /// <summary>
        /// Throw animation
        /// </summary>
        private Animation throwAnimation;

        /// <summary>
        /// Duration of throw animation
        /// </summary>
        private float throwAnimationTime;

        /// <summary>
        /// Throw timer
        /// </summary>
        private float throwTimer;

        /// <summary>
        /// Time between throws
        /// </summary>
        private const float TimeBetweenThrows = 3f;

        /// <summary>
        /// Impulse to apply to grenade
        /// </summary>
        private Vector2 throwImpulse = Vector2.Zero;
        #endregion

        #region Constructors
        public GrenadeSoldier()
        { }
        #endregion

        #region Public Overridden Methods
        public override void Initialize(GameWorldArea gameWorld, Player player, Vector2 spawnPoint, Map map, string name)
        {
            this.gameWorld = gameWorld;
            this.spawnPoint = spawnPoint;
            this.map = map;
            this.player = player;
            this.name = name;

            flip = SpriteEffects.None;

            // setup attributes
            hitPoints = 30;
            AttackStrength = 10;
            MoveSpeed = 0.5f;
            MaxWaitTime = 2.5f;

            // set view distance
            viewDistance = map.Tile_Size * 20;

            // enemy is hostile
            isHostile = true;

            LoadContent("Enemies/GrenadeSoldier");
            Reset();
        }

        /// <summary>
        /// Load all entity content
        /// </summary>
        public override void LoadContent(string entity)
        {
            // Load animated textures.
            idleAnimation = new Animation(GameWorldComponent.MapContent.Load<Texture2D>("Sprites/Enemies/GrenadeSoldier/Idle"), 0.15f, 7, true);
            throwAnimation = new Animation(GameWorldComponent.MapContent.Load<Texture2D>("Sprites/Enemies/GrenadeSoldier/Throw"), 0.15f, 7, true);

            throwAnimationTime = throwAnimation.FrameCount * throwAnimation.FrameTime;

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

            killedSound = "Charger_Death";

            bodyOffset = (localBounds.Height / 2);
            body = BodyFactory.Instance.CreateEllipseBody(GameWorldComponent.PhysicsSimulator, width / 2, height / 2, 100);
            body.MomentOfInertia = float.MaxValue;
            geom = GeomFactory.Instance.CreateEllipseGeom(GameWorldComponent.PhysicsSimulator, body, width / 2, height / 2, 200);
            geom.FrictionCoefficient = 3f;

            List<string> tags = new List<string>();
            tags.Add("NPC");

            geom.Tag = tags as object;
            geom.CollisionCategories = CollisionCategory.NPC;
            geom.CollidesWith = CollisionCategory.World;

            throwTimer = TimeBetweenThrows + Rand.GetRandomFloat(0f, 1.5f);
        }

        /// <summary>
        /// Perform AI Routines
        /// </summary>
        /// <remarks>
        /// GrenadeSoldier entity considers throwing a grenade at the player.
        /// If specific conditions are met, a grenade will be thrown.
        /// </remarks>
        public override void Think(GameTime gameTime)
        {
            if (isFrozen)
                return;

            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            // set our face direction to face the player
            if (body.Position.X < player.Body.Position.X)
                direction = FaceDirection.Left;
            else
                direction = FaceDirection.Right;

            // if we cannot see the player, reset the throw timer and return
            if (!CanSee())
            {
                throwTimer = TimeBetweenThrows + Rand.GetRandomFloat(0f, 1.5f);
                return;
            }

            // start the throw process if we are ready
            if (throwTimer <= 0f)
            {

                // if we have not yet gotten our throw impulse, try to get it
                if (throwImpulse == Vector2.Zero)
                {
                    // calculate angle
                    float v = 1600f;
                    float g = GameWorldComponent.PhysicsSimulator.Gravity.Y * 1f;
                    float x = Math.Abs(player.Position.X - body.Position.X);
                    float y = Math.Abs(player.Position.Y - body.Position.Y);

                    if (player.Position.Y > body.Position.Y)
                        y *= -1;

                    // use the Angle θ required to hit coordinate (x,y) formula in order to
                    // find theta
                    double radical = Math.Pow(v, 4) - g * (g * Math.Pow(x, 2) + 2 * y * Math.Pow(v, 2));

                    // only proceed if our result will not be imaginary
                    if (radical >= 0)
                    {
                        // play throw animation and set animation timer
                        sprite.PlayAnimation(throwAnimation);
                        attackAnimTimer = throwAnimationTime;

                        // no real use for the higher arc angle as of yet
                        //var numeratorPlus = Math.Pow(v, 2) + Math.Sqrt(radical);

                        double numeratorMinus = Math.Pow(v, 2) - Math.Sqrt(radical);
                        double denominator = g * x;

                        // no real use for the higher arc angle as of yet
                        //var thetaPlus = Math.Atan(numeratorPlus / denominator);

                        // we will be using the lower angle
                        double thetaMinus = Math.Atan(numeratorMinus / denominator);

                        // get angle theta
                        double theta;
                        theta = thetaMinus;

                        // get our unit vector from theta
                        Vector2 unitV = new Vector2((float)Math.Cos(theta), -(float)Math.Sin(theta));

                        // get our impulse from our velocity scalar and our unit vector
                        Vector2 impulse = Vector2.Multiply(unitV, v);

                        // reverse the x component if the player is behind the thrower
                        if (player.Position.X < body.Position.X)
                            impulse.X *= -1;

                        // set impulse
                        throwImpulse = impulse;
                    }
                    else // if we reach this, our radical was imaginary, and our throw is impossible.  reset the timer
                    {
                        throwTimer = TimeBetweenThrows + Rand.GetRandomFloat(0f, 1.5f);
                    }
                }
                else // we have our throw impulse
                {
                    // since we have our throw impulse, we need to wait for the
                    // throw animation to progress half way before applying the impulse

                    if (attackAnimTimer <= throwAnimationTime / 2)
                    {
                        // create grenade, and apply the throw impulse
                        Grenade grenade = new Grenade(this);
                        gameWorld.EntityList.Add(grenade);
                        grenade.Body.ApplyImpulse(throwImpulse);

                        // reset throw impulse
                        throwImpulse = Vector2.Zero;

                        // reset throw timer
                        throwTimer = TimeBetweenThrows + Rand.GetRandomFloat(0f, 1.5f);
                    }
                }
            }
            else
            {
                // decrement throw timer
                throwTimer = Math.Max(0.0f, throwTimer - elapsed);
            }
        }

        /// <summary>
        /// Die
        /// </summary>
        public override void Die(GameTime gameTime)
        {
            if (IsAlive)
            {
                // play death sound
                if (killedSound != null)
                    SoundManager.PlaySound(killedSound, 1f);

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

            base.Die(gameTime);
        }

        /// <summary>
        /// Think, then update
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            if (IsAlive)
            {
                if (!isFrozen)
                {
                    (geom.Tag as List<string>)[0] = "Grenade Soldier";
                    geom.CollisionCategories = CollisionCategory.NPC;

                    Think(gameTime);
                    DamagePlayer(gameTime);
                }
                else
                {
                    (geom.Tag as List<string>)[0] = "Frozen";
                    geom.CollisionCategories = CollisionCategory.FrozenNPC;
                }

                base.Update(gameTime);
            }
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Check if the player is in sight
        /// </summary>
        private new bool CanSee()
        {
            // if the player is dead, or not in the water, halt
            if (!player.IsAlive)
                return false;

            // first, if the player is too far away ignore all checking
            if (Vector2.Distance(this.Position, player.Position) > viewDistance)
                return false;

            // cast a ray to decide whether or not we can see the player
            List<GeomPointPair> intersectingGeoms;

            intersectingGeoms = RayHelper.LineSegmentAllGeomsIntersect(
                body.Position,
                player.Body.Position,
                GameWorldComponent.PhysicsSimulator,
                true);

            // remove the characters own geometry from the ray collision list
            for (int i = 0; i < intersectingGeoms.Count; i++)
            {
                if (intersectingGeoms[i].Geom == this.geom)
                {
                    intersectingGeoms.RemoveAt(i);
                    break; ;
                }
            }

            // sort geoms list to get the closest geom collision
            intersectingGeoms.Sort(delegate(GeomPointPair g1, GeomPointPair g2)
            {
                return g1.Geom.Position.X.CompareTo(g2.Geom.Position.X);
            });

            // if there are geoms left, test them for the player
            if (intersectingGeoms.Count > 0)
            {
                if (intersectingGeoms[0].Geom == player.Geom)
                    return true;
                else
                    return false;
            }

            return false;
        }
        #endregion

        #region Grenade Class
        /// <summary>
        /// Nested class.  Grenade thrown by grenade soldier
        /// </summary>
        private class Grenade : PhysicsObject
        {
            #region Fields
            /// <summary>
            /// Throwing soldier
            /// </summary>
            private GrenadeSoldier owner;

            /// <summary>
            /// Bounding box used for explosion collision
            /// </summary>
            private Rectangle explosionBox;

            /// <summary>
            /// Timer for explosion duration
            /// </summary>
            private float explosionTimer;

            /// <summary>
            /// Maximum explosion duration
            /// </summary>
            private const float MaxExplosionTime = 0.5f;

            /// <summary>
            /// Size of explosion
            /// </summary>
            private const int explosionSize = 20;

            /// <summary>
            /// Strength of explosion
            /// </summary>
            private const float explosionStrength = 5f;
            #endregion

            #region Constructors
            public Grenade(GrenadeSoldier owner)
            {
                this.owner = owner;
                Initialize(owner.gameWorld, owner.player, owner.Body.Position, owner.Map, "Grenade");
            }
            #endregion

            #region Public Overridden Methods
            /// <summary>
            /// Initialize
            /// </summary>
            public override void Initialize(GameWorldArea gameWorld, Player player, Vector2 spawnPoint, Map map, string name)
            {
                this.gameWorld = gameWorld;
                this.spawnPoint = spawnPoint;
                this.map = map;
                this.player = player;
                this.name = name;

                flip = SpriteEffects.None;

                LoadContent("Enemies/GrenadeSoldier/Grenade");
                Reset();
            }

            /// <summary>
            /// Load all content
            /// </summary>
            public override void LoadContent(string entity)
            {
                // Load animated textures.
                animation = new Animation(GameWorldComponent.MapContent.Load<Texture2D>("Sprites/Enemies/GrenadeSoldier/Grenade"), 1f, 1, false);

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

                bodyOffset = (localBounds.Height / 2);
                body = BodyFactory.Instance.CreateEllipseBody(GameWorldComponent.PhysicsSimulator, width / 2, height / 2, 1f);
                body.MomentOfInertia = float.MaxValue;
                geom = GeomFactory.Instance.CreateEllipseGeom(GameWorldComponent.PhysicsSimulator, body, width / 2, height / 2, 1);
                geom.FrictionCoefficient = 3f;

                List<string> tags = new List<string>();
                tags.Add("EnemyPhysObj");

                geom.Tag = tags as object;
                geom.CollisionCategories = CollisionCategory.EnemyPhysObj;
                geom.CollidesWith = CollisionCategory.World & CollisionCategory.Player;

                geom.OnCollision += OnCollision;
            }

            /// <summary>
            /// Update method
            /// </summary>
            public override void Update(GameTime gameTime)
            {
                float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

                // if the explosion timer is running, check for collisions
                if (explosionTimer > 0f)
                {
                    CheckForPlayerCollision(gameTime);
                    explosionTimer = Math.Max(0.0f, explosionTimer - elapsed);
                }

                // if we have a collision box set up, kill the grenade when the timer hits zero
                if (explosionBox != Rectangle.Empty)
                    if (explosionTimer <= 0f)
                        Die(gameTime);

 	            base.Update(gameTime);
            }

            /// <summary>
            /// Check for player collision with explosion
            /// </summary>
            private void CheckForPlayerCollision(GameTime gameTime)
            {
                if (explosionBox.Intersects(player.BoundingRectangle))
                    player.TakeDamage(gameTime, explosionStrength);
            }
            #endregion

            #region Private Methods
            /// <summary>
            /// Collision event
            /// </summary>
            private bool OnCollision(Geom geometry1, Geom geometry2, ContactList contactList)
            {
                // make explosion effect
                GameWorldComponent.GameParticleManager.MakeExplosion(body.Position, 1f);

                // play explosion sfx
                SoundManager.PlaySound("Explosion", 1f);

                // set up bounding box for explosion
                explosionBox.X = (int)body.Position.X - (explosionSize / 2);
                explosionBox.Y = (int)body.Position.Y - (explosionSize / 2);
                explosionBox.Width = explosionSize;
                explosionBox.Height = explosionSize;

                // start explosion timer
                explosionTimer = MaxExplosionTime;

                // disable body
                body.Enabled = false;

                return true;
            }
            #endregion
        }
        #endregion
    }
}