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 } }