/*
* Boss.cs
* Authors: August Zinsser, Adam Nabinger
* Copyright (c) 2007-2008 Cornell University
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using TACParticleEngine.Particles;
using Random=Util.Random;
namespace MaritimeDefender
{
///
/// The version of the photon torpedo that the boss fires
///
class BossLaser : PhotonTorpedo
{
#region Creation
///
/// Create a new boss projectile
///
/// The position for the boss to spawn at
/// The depth for the boss to spawn at
/// The depth for the boss to die at
/// Reference to the current Maritime Defender game screen
public BossLaser(Vector3 spawnLocation, float spawnDepth, float killDepth, MaritimeDefender game)
: base(false, true, 0, spawnLocation, .5f, .5f, .5f, -5f, spawnDepth, killDepth, game)
{
mPos.Z = spawnDepth;
mFriendly = false;
mDamage = 10;
}
#endregion
#region Management
///
/// Loads in needed content
///
/// The current content manager
public override void LoadContent(Microsoft.Xna.Framework.Content.ContentManager content)
{
base.LoadContent(content);
mPhotonTorpedo = mGame.MMParticleManager.Load("ParticleEffects/BossPhotonTorpedo");
mPhotonTorpedo.SetEnginePosition(MaritimeDefender.ConvertPosition(mPos));
mPhotonTorpedo.SetActive(true);
}
#endregion
#region Update
///
/// Updates the position of the torpedoes
///
/// Time that has passed in game
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
// Update the photon's position
if (mIsGoingAway && mPos.Z > mKillDepth || !mIsGoingAway && mPos.Z < mKillDepth)
{
mAlive = false;
}
}
#endregion
}
///
/// The rockets that the boss fires. This extends PhotonTorpedo for the sake of plugging into
/// MaritimeDefender without reworking how projectiles work.
///
class BossRocket : PhotonTorpedo
{
#region Properties
///
/// Gets a basic cubic area of size 1
///
public override Vector3 Size { get { return Vector3.One; } }
#endregion
#region Creation
///
/// Create a new Rocket
///
/// The position for the boss to spawn at
/// The amount of rotation for the rocket texture image
/// The depth for the boss to spawn at
/// The depth for the boss to die at
/// Reference to the current Maritime Defender game screen
public BossRocket(Vector3 spawnLocation, float rotation, float spawnDepth, float killDepth, MaritimeDefender game)
: base(false, true, 0, spawnLocation, .125f, .125f, 1f, -5f, spawnDepth, killDepth, game)
{
mRot = rotation;
mPos.Z = spawnDepth;
mFriendly = false;
mDamage = 15;
Visible = true;
}
#endregion
#region Management
///
/// Loads in needed content
///
/// The current content manager
public override void LoadContent(Microsoft.Xna.Framework.Content.ContentManager content)
{
mTexturePath = "MiniGames/MMRocket";
base.LoadContent(content);
mPhotonTorpedo = mGame.MMParticleManager.Load("ParticleEffects/RocketTrail");
mPhotonTorpedo.SetEnginePosition(MaritimeDefender.ConvertPosition(mPos));
mPhotonTorpedo.SetActive(true);
}
#endregion
#region Update
///
/// Updates the position of the rocket
///
/// Time that has passed in game
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
mVel.Z *= 1.01f;
}
#endregion
}
///
/// The rocket pods on the outside of the boss's missile ring
///
internal class RocketPod : Entity
{
#region Fields
///
/// The amount of life this has before it is destroyed
///
public int HitPoints;
///
/// The position offset from the boss this is attached to
///
public Vector3 Offset;
///
/// The degree of rotation for the pod and any rocket that is shot out of it
///
public float PodRotation;
///
/// Reference to the current Maritime Defender game screen
///
public MaritimeDefender mGame;
// Particle effect used for indicating that the pod was hit
private ParticleEngine mPodHit;
// Particle effect used for indicating that the pod has been destroyed
private ParticleEngine mPodDeath;
#endregion
#region Predicates
///
/// Let's us know when the rocket pod is considered dead for management purposes
///
public static readonly Predicate Dead = delegate(RocketPod o)
{
return o.HitPoints <= 0;
};
#endregion
#region Creation
///
/// Creates a new rocket pod on the boss
///
/// Reference to the current Maritime Defender game screen
/// The path name of the texture to represent this
public RocketPod(MaritimeDefender game, string textureName)
: base(textureName)
{
mGame = game;
LoadContent(game.Content);
}
#endregion
#region Management
///
/// Loads in needed content
///
/// The current content manager
public override void LoadContent(Microsoft.Xna.Framework.Content.ContentManager content)
{
mPodHit = mGame.MMParticleManager.Load("ParticleEffects/Spark");
mPodDeath = mGame.MMParticleManager.Load("ParticleEffects/Explosion");
base.LoadContent(content);
}
///
/// Cleans up any unmanaged objects
///
public override void UnloadContent()
{
mPodDeath.ToDestory = true;
mPodHit.ToDestory = true;
base.UnloadContent();
}
#endregion
#region GetHit
///
/// Applies a successful hit to the rocket pod and processes it accordingly
///
/// The amount of damage to take
/// The position in which it was hit from
public void GetHit(int damage, Vector3 hitFromPosition)
{
HitPoints -= damage;
if (HitPoints <= 0)
{
mPodDeath.SetEnginePosition(MaritimeDefender.ConvertPosition(mPos));
mPodDeath.SetActive(true);
mPodDeath.ToDestory = true;
mPodHit.ToDestory = true;
mAlive = false;
}
else
{
mPodHit.SetEnginePosition(MaritimeDefender.ConvertPosition(mPos));
mPodHit.SetActive(true);
}
}
#endregion
}
///
/// This represents the boss in Meteor Madness. It maintains an internal state machine and
/// gets a handle to the meteor madness game on update in order to spawn attacks.
///
class Boss : UFO
{
#region Enums
///
/// Defines all the various states that the boss can be in
///
public enum States {
///
/// Defines when the boss is approaching the player
///
Approach,
///
/// Defines when the boss is idle
///
Idle,
///
/// Defines when the boss is boosting
///
Boost,
///
/// Defines when the boss has stopped boosting
///
UnBoost,
///
/// Defines when the boss is turning
///
Turning,
///
/// Defines when the boss is attacking from afar
///
FarAttack,
///
/// Defines when the boss is attacking up close
///
NearAttack,
///
/// Defines when the boss is dying
///
Dying
};
#endregion
#region Constants
// Defines the starting number of pods around the boss
private const int NUM_OF_PODS = 24;
// Defines the offset position for the boss engines
private const float ENGINE_OFFSET = 0.3f;
// Constant for how long the delay between consecutive shots is for the boss phase.
private const float NEAR_SHOT_DELAY = .5f;
#endregion
#region Fields
// Particle effect to visualize the 1st engine on the boss
private ParticleEngine mBossEngine1;
// Particle effect to visualize the 2nd engine on the boss
private ParticleEngine mBossEngine2;
// Particle effect to visualize the 3rd engine on the boss
private ParticleEngine mBossEngine3;
// Particle effect to show when the boss has been hit
private ParticleEngine mBossHit;
// Particle effect to show when the boss has been killed
private ParticleEngine mBossDeath;
///
/// Handle to the meteor madness game
///
protected readonly MaritimeDefender mGame;
///
/// Parts of the ship that fly off
///
protected List mDebris;
///
/// State machine for AI logic
///
protected States mState;
///
/// Some states are timed
///
protected float mStateTimer;
///
/// Must be destroyed before any other damage can occur
///
protected int mShields;
///
/// Maximum life of the shields
///
protected int mMaxShields;
///
/// Used to calculate life percentage
///
protected int mMaxHitPoints;
///
/// Z-Depth of the ship in the idle state
///
protected float mIdleZ;
///
/// The z-Depth to start decelerating
///
protected float mBoostZ;
///
/// The boss actually gets smaller after its shields are gone
///
protected float mShieldedDepth;
///
/// Each rocket pod on the ring
///
protected List mRocketPods;
///
/// If the ring is still attached
///
protected bool mRingGone;
///
/// True when the boss needs to turn around
///
protected bool mNeedsToTurn;
///
/// Counts down as the ship is turning around
///
protected float mTurningTimer;
///
/// Whether the ship can be hit
///
protected bool mVulnerable;
///
/// Counts down to from the player "killing" the boss to the game recognizing the boss is dead
///
protected float mDeathTimer;
///
/// Shake when dying
///
protected Vector3 mShake;
///
/// Counts down to shooting
///
protected float mShotTimer;
///
/// The last rocket that was fired
///
protected int mLastRocket;
///
/// Time between each far attack
///
protected Queue mFarAttackDelays;
///
/// Wait until the user has read the encounter dialog to begin attacking
///
protected bool mWaitingForDialog;
///
/// Defines when the an entity is past the near clipping plane so that it will be drawn
///
private readonly Predicate pastMinZ;
#endregion
#region Properties
///
/// Gets the percent of shield life left
///
public float ShieldLife { get { return (float)mShields / (float)mMaxShields; } }
///
/// Gets the percent of pods left
///
public float RocketPodLife { get { return mRocketPods.Count / (float)NUM_OF_PODS; } }
///
/// Gets the percent of life left
///
public float HullLife { get { return mHitPoints / mMaxHitPoints; } }
#endregion
#region Creation
///
/// Creates a new boss
///
/// Reference to the current Maritime Defender game screen
/// The width of the boss
/// The height of the boss
/// The depth of the boss
/// The depth the boss is at when idle
/// The depth at which the boss stops boosting
/// How many hit points before the ship is destroyed. This does not include shields.
/// How much damage the shields can take before the main body can be damaged
public Boss(MaritimeDefender game, float width, float height, float depth, float idleZ, float boostZ, int shields, int hitPoints)
: base(null, width, height, depth, hitPoints)
{
mGame = game;
float minZ = game.GameBoard.MinZ;
pastMinZ = delegate(Entity o) { return o.Z < minZ; };
mSpace3DSize = new Vector3(width, height, depth);
mSize.Z = mSize.Y;
mShieldedDepth = depth;
mIdleZ = idleZ;
mPos.Z = boostZ * 10f;
mBoostZ = boostZ;
mMaxShields = shields;
mMaxHitPoints = hitPoints;
mShields = shields;
mShake = Vector3.Zero;
mAngVel = 0f;
mWaitingForDialog = true;
LoadContent(mGame.Content);
mFarAttackDelays = new Queue();
for (int i = 0; i < 36; i++)
{
mFarAttackDelays.Enqueue(.05f);
}
mFarAttackDelays.Enqueue(3f);
//mState = States.Approach;
mState = States.FarAttack;
mVulnerable = false;
mNeedsToTurn = false;
}
#endregion
#region Management
///
/// Loads in any needed information for all content dependent objects
///
/// Reference to the current content manager for loading content
public override void LoadContent(Microsoft.Xna.Framework.Content.ContentManager content)
{
Texture2D bossSpriteSheet = content.Load(@"MiniGames\MMBossSpriteSheet");
mSprite = new Animation(new Vector2(bossSpriteSheet.Width >> 1, bossSpriteSheet.Height >> 1));
mSprite.AddClip("Phase1", bossSpriteSheet, 1, 0, 0);
mSprite.AddClip("Phase2", bossSpriteSheet, 1, 1, 0);
mSprite.AddClip("Phase3", bossSpriteSheet, 1, 2, 0);
mSprite.Play("Phase1");
mDebris = new List();
mDebris.Add(new Entity(@"MiniGames\MMBossDebrisUL", this.Position, this.Size));
mDebris.Add(new Entity(@"MiniGames\MMBossDebrisUR", this.Position, this.Size));
mDebris.Add(new Entity(@"MiniGames\MMBossDebrisBL", this.Position, this.Size));
mDebris.Add(new Entity(@"MiniGames\MMBossDebrisBR", this.Position, this.Size));
foreach (Entity e in mDebris)
{
e.Visible = false;
e.LoadContent(content);
}
string[] rocketPods = new string[3];
rocketPods[0] = @"MiniGames\RocketPodA";
rocketPods[1] = @"MiniGames\RocketPodB";
rocketPods[2] = @"MiniGames\RocketPodC";
mRocketPods = new List();
mGame.GameBoard.mRocketPod = mRocketPods;
for (int i = 0; i < NUM_OF_PODS; i++)
{
float thisArcRotation = (i * MathHelper.TwoPi / NUM_OF_PODS);
Vector3 thisPositionOffset = new Vector3((float)Math.Cos(thisArcRotation), (float)Math.Sin(thisArcRotation), 0f);
// Hardcode adjust values so they end up in the right place
thisPositionOffset.X = thisPositionOffset.X * 1.15f;
thisPositionOffset.Y = thisPositionOffset.Y * 1.3f;
thisPositionOffset.Z = thisPositionOffset.Y * .1f;
RocketPod newPod = new RocketPod(mGame, rocketPods[i % 3]);
newPod.Position = mPos + thisPositionOffset;
newPod.Offset = thisPositionOffset;
float rocketWidth = .064f * mSpace3DSize.X;
float rocketHeight = rocketWidth * newPod.Height / newPod.Width;
newPod.Size = new Vector3(rocketWidth, rocketHeight, rocketHeight);
newPod.HitPoints = 30;
newPod.PodRotation = thisArcRotation;
mRocketPods.Add(newPod);
}
mBossEngine1 = mGame.MMParticleManager.Load("ParticleEffects/BossEngine");
mBossEngine1.SetActive(true);
mBossEngine2 = mGame.MMParticleManager.Load("ParticleEffects/BossEngine");
mBossEngine2.SetActive(true);
mBossEngine3 = mGame.MMParticleManager.Load("ParticleEffects/BossEngine");
mBossEngine3.SetActive(true);
mBossHit = mGame.MMParticleManager.Load("ParticleEffects/Shield");
mBossDeath = mGame.MMParticleManager.Load("ParticleEffects/Explosion");
base.LoadContent(content);
}
#endregion
#region Update
///
/// Update boss logic
///
/// Time that has passed in game
public override void Update(GameTime gameTime)
{
float dT = (float)gameTime.ElapsedGameTime.TotalSeconds;
mStateTimer -= dT;
mTurningTimer -= dT;
mDeathTimer -= dT;
mShotTimer -= dT;
mVulnerable = true;
if (mState == States.Approach || mState == States.UnBoost || mNeedsToTurn)
mVulnerable = false;
// Move the shields
mBossHit.SetEnginePosition(MaritimeDefender.ConvertPosition(new Vector3(X, Y, 0)));
// Update Engine positions
mBossEngine1.SetEnginePosition(MaritimeDefender.ConvertPosition(new Vector3(X - ENGINE_OFFSET, Y + ENGINE_OFFSET, Z)));
mBossEngine2.SetEnginePosition(MaritimeDefender.ConvertPosition(new Vector3(X, Y + 0.05f, Z)));
mBossEngine3.SetEnginePosition(MaritimeDefender.ConvertPosition(new Vector3(X + ENGINE_OFFSET, Y + ENGINE_OFFSET, Z)));
// Destroy ring if necessary
if (mRocketPods.Count == 0 && !mRingGone)
{
mRingGone = true;
mNeedsToTurn = true;
mVulnerable = false;
mSprite.Play("Phase2");
mBossDeath.SetEnginePosition(MaritimeDefender.ConvertPosition(new Vector3(X, Y, Z)));
mBossDeath.SetActive(true);
mDebris[0].Play("Piece1");
mDebris[1].Play("Piece2");
mDebris[2].Play("Piece3");
mDebris[3].Play("Piece4");
mDebris[0].Position = new Vector3(-.7f, -.9f, -.0001f);
mDebris[1].Position = new Vector3(.7f, -.9f, -.0002f);
mDebris[2].Position = new Vector3(-.7f, 1.0f, -.0003f);
mDebris[3].Position = new Vector3(.7f, 1.0f, -.0004f);
for (int i = 0; i < mDebris.Count; i++)
{
mDebris[i].Size = Vector3.Multiply(Size, .5f);
mDebris[i].Position += Position;
mDebris[i].Velocity = mDebris[i].Position;
mDebris[i].dX *= Random.NextFloat(.01f, 1.0f);
mDebris[i].dY *= Random.NextFloat(.01f, 1.0f);
mDebris[i].dZ = Random.NextFloat(-.5f, -.05f);
mDebris[i].Visible = true;
mDebris[i].FadeOut(4f);
mGame.GameBoard.AddEntity(mDebris[i]);
}
}
foreach (Entity d in mDebris)
{
d.Update(gameTime);
d.Velocity *= 1.02f;
}
mDebris.RemoveAll(pastMinZ);
// Perform logic based on state
switch (mState)
{
#region Approach
case States.Approach:
// The initial state
if (mPos.Z > mIdleZ)
{
float distanceLeft = mPos.Z / mIdleZ;
mPos.Z -= distanceLeft * .03f + .01f;
}
if (mPos.Z < mIdleZ)
mPos.Z = mIdleZ;
// GOTO: idle
if (mPos.Z == mIdleZ)
{
mState = States.Idle;
mStateTimer = 10f;
}
break;
#endregion
#region Idle
case States.Idle:
// Sit there and let the player hit the boss (or wait for the player to finish reading dialog)
if (mStateTimer < 0 && !mWaitingForDialog || mRingGone)
{
mState = States.Boost;
mStateTimer = Random.NextFloat(3f, 4f);
}
// GOTO: boost, close attack
break;
#endregion
#region Boost
case States.Boost:
// Speed away from the player
if (mPos.Z < mBoostZ)
{
// Speed up
mVel.Z += .1f;
}
else
{
// Slow down
if (mVel.Z > 0)
mVel.Z *= .5f;
if (mVel.Z < .1f)
mVel.Z = 0;
}
// GOTO: Far attack, Turn around
if (mStateTimer < 0)
{
if (mNeedsToTurn)
{
mBossEngine1.SetActive(false);
mBossEngine2.SetActive(false);
mBossEngine3.SetActive(false);
mTurningTimer = 1f;
FadeOut(1f);
mNeedsToTurn = false;
mState = States.Turning;
}
else
{
mStateTimer = Random.NextFloat(6f, 12f);
mState = States.FarAttack;
}
}
break;
#endregion
#region FarAttack
case States.FarAttack:
// Attack the player with rockets
if (mShotTimer < 0 && mStateTimer > 2f)
{
if (mLastRocket >= mRocketPods.Count)
mLastRocket = 0;
RocketPod curRocket = mRocketPods[mLastRocket++];
BossRocket bossRocket = new BossRocket(curRocket.Position,
curRocket.PodRotation - MathHelper.PiOver2, Z, 0, mGame);
mGame.Projectiles.Add(bossRocket);
mGame.GameBoard.mPhotonTorpedos.Add(bossRocket);
mShotTimer = mFarAttackDelays.Dequeue();
mFarAttackDelays.Enqueue(mShotTimer);
}
// GOTO: Unboost
if (mStateTimer < 0)
{
// Let the player catch up and then resume idle
mState = States.UnBoost;
}
break;
#endregion
#region UnBoost
case States.UnBoost:
// Return from a boost to idle state
if (mPos.Z > mIdleZ)
{
mPos.Z -= 1f;
}
if (mPos.Z < mIdleZ)
mPos.Z = mIdleZ;
// GOTO: idle, near attack
if (mPos.Z == mIdleZ)
{
if (mRingGone)
{
mAngVel = (MathHelper.Pi / .96f);
mState = States.NearAttack;
}
else
{
mState = States.Idle;
mStateTimer = Random.NextFloat(10f, 15f);
}
}
break;
#endregion
#region Turning
case States.Turning:
// GOTO: UnBoost
if (mTurningTimer <= 0)
{
mSprite.Play("Phase3");
FadeIn(1f);
mState = States.UnBoost;
}
break;
#endregion
#region NearAttack
case States.NearAttack:
Opacity = 1f;
mVisible = true;
// Attack the player relentlessly!
if (mTurningTimer < 0)
{
mAngVel *= -1f;
mTurningTimer = Random.NextFloat(5f, 10f);
}
if (mShotTimer < 0)
{
mShotTimer = NEAR_SHOT_DELAY;
// Fire a 3-shot spread
for (int i = 0; i < 3; i++)
{
BossLaser shot = new BossLaser(new Vector3(.5f * (float)Math.Cos(mRot + MathHelper.PiOver4 * i / 4),
.5f * (float)Math.Sin(mRot + MathHelper.PiOver4 * i / 4), this.Position.Z), Position.Z, -1f, mGame);
mGame.Projectiles.Add(shot);
}
}
break;
#endregion
#region Dying
case States.Dying:
// Shake, spin and die
mShake = new Vector3(
.05f * (float)Math.Cos(gameTime.ElapsedGameTime.TotalSeconds * 25f),
.01f * (float)Math.Sin(gameTime.ElapsedGameTime.TotalSeconds * 5f),
0f);
mAngVel *= 1.01f;
if (mDeathTimer <= 0)
{
mBossHit.SetActive(false);
mBossEngine1.SetActive(false);
mBossEngine2.SetActive(false);
mBossEngine3.SetActive(false);
mBossDeath.ToDestory = true;
mBossHit.ToDestory = true;
mBossEngine1.ToDestory = true;
mBossEngine2.ToDestory = true;
mBossEngine3.ToDestory = true;
mAlive = false;
}
else
{
if (!mBossDeath.EmitterAlive)
{
mBossDeath.SetEnginePosition(new Vector3(X, Y, 0f));
mBossDeath.SetActive(true);
}
}
break;
#endregion
}
Position += mShake;
base.Update(gameTime);
// Update rocket pods
foreach (RocketPod rocketPod in mRocketPods)
{
rocketPod.Update(gameTime);
rocketPod.Position = mPos + rocketPod.Offset;
}
mRocketPods.RemoveAll(RocketPod.Dead);
}
#endregion
#region Getters
///
/// Returns a list of exposed rocketpods to test for collision
///
///
public List GetVulnerableRocketPods()
{
if (mVulnerable && mShields <= 0)
{
return mRocketPods;
}
return null;
}
#endregion
#region Setters
///
/// Call this to allow the boss to start fighting
///
public void Start()
{
mWaitingForDialog = false;
}
#endregion
#region GetHit
///
/// Deals the specified amount of damage to this ship, potentially killing it. The boss may only be vulnerable at certain angles,
/// so the angle at which it is hit is important.
///
public void GetHit(int damage, Vector3 hitFromPosition)
{
if (mState == States.Dying)
{
return;
}
// Shields must be taken down first
if (mShields > 0 && mVulnerable)
{
mShields -= damage;
// Redden the shields to indicate that they are low
float percentLeft = (float)mShields / (float)mMaxShields;
SoundManager.SoundManager.PlayEffect("Zzt");
mBossHit.SetEngineColor(new Vector4(1 - percentLeft, 0, percentLeft, 0.2f), false);
mBossHit.SetActive(true);
}
else if (mVulnerable && mRingGone)
{
if (mRocketPods.Count <= 0)
{
// Rocket pods are gone, so go ahead and damage the ship
mHitPoints -= damage;
// Display the damage
SoundManager.SoundManager.PlayEffect("Smash");
mBossDeath.SetEnginePosition(MaritimeDefender.ConvertPosition(hitFromPosition));
mBossDeath.SetActive(true);
if (mHitPoints <= 0)
{
mState = States.Dying;
mDeathTimer = 5f;
FadeOut(5f);
mBossDeath.SetEnginePosition(MaritimeDefender.ConvertPosition(new Vector3(X, Y, 0f)));
mBossDeath.SetActive(true);
}
}
}
}
#endregion
}
}