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