/* * Boss.cs * Authors: August Zinsser * * Copyright Matthew Belmonte 2007 */ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Pina3D.Particles; using tAC_Engine; namespace Astropolis { /// /// The version of the photon torpedo that the boss fires /// class BossLaser : PhotonTorpedo { /// /// Create a new boss projectile /// /// /// public BossLaser(Vector3 spawnLocation, float spawnDepth, float killDepth) : base(true, Color.DarkSalmon, 0, spawnLocation, .5f, .5f, .5f, -15f, spawnDepth, killDepth) { mPos.Z = spawnDepth; mFriendly = false; mDamage = 25; } /// /// Scroll the torpedoes /// public override void Update() { base.Update(); // Update the photon's position if (mIsGoingAway && mPos.Z > mKillDepth || !mIsGoingAway && mPos.Z < mKillDepth) mAlive = false; // Adjust the Particles Rectangle proj = Space3D.Project(mPos.X, mPos.Y, mPos.Z, mSpace3DSize.X, mSpace3DSize.Y, -1f); float zDepth = mPos.Z / mSpawnDepth; if (mIsGoingAway) zDepth = mPos.Z / mKillDepth; float scale = 1; mTrailEmitter.Position3D = new Vector3(proj.X, proj.Y, MathHelper.Clamp(zDepth, 0f, 1f)); mTrailEmitter.SpawnRate = (Math.Max(zDepth, .1f)) * .1f; mHeadEmitter.RenderScale = scale * 2; mHeadEmitter.Transform = Matrix.CreateScale(scale) * Matrix.CreateTranslation(proj.X, proj.Y, MathHelper.Clamp(1 - scale, 0f, 1f)); if (mIsGoingAway) { mTrailEmitter.RegularEmissionType.Modifiers[0].InitialOpacity = scale; mTrailEmitter.RegularEmissionType.Modifiers[0].FinalOpacity = scale; mHeadEmitter.RegularEmissionType.Modifiers[0].InitialOpacity = scale; } // Kill the particle effect if the torpedo dies if (!mAlive) { mTrailEmitter.Alive = false; mHeadEmitter.Alive = false; } } } /// /// The rockets that the boss fires. This extends PhotonTorpedo for the sake of plugging into MeteorMadness without reworking how /// projectiles work. /// class BossRocket : PhotonTorpedo { public override Vector3 CollisionSize { get { return Vector3.One; } } // Return a large collision box /// /// Create a new Rocket /// /// /// /// public BossRocket(Vector3 spawnLocation, float rotation, float spawnDepth, float killDepth) : base(true, Color.Gray, 0, spawnLocation, .125f, .125f, 1f, -5f, spawnDepth, killDepth) { mRot = rotation; mPos.Z = spawnDepth; mFriendly = false; mDamage = 15; mHeadEmitter.KillAllEmissions(); Sparx.RemoveEmitter(mHeadEmitter); Texture2D rocketTexture = TextureManager.Load(@"Content\MiniGames\MMRocket"); mSprite = new Animation(new Vector2(rocketTexture.Width, rocketTexture.Height)); mSprite.AddClip("Rocket", rocketTexture, 1, 0f); mSprite.Play("Rocket"); Sparx.RemoveEmitter(mTrailEmitter); mTrailEmitter = Sparx.LoadParticleEffect(@"Content\Particle Effects\SmokeTrail.spx"); Rectangle proj = Space3D.Project(mPos.X, mPos.Y, mPos.Z, mSpace3DSize.X, mSpace3DSize.Y, -1f); mTrailEmitter.Position3D = new Vector3(proj.X, proj.Y, -mPos.Z / killDepth); Sparx.AddEmitter(mTrailEmitter); } /// /// Scroll the torpedoes /// public override void Update() { base.Update(); mVel.Z *= 1.01f; float zDepth = mPos.Z / mSpawnDepth; float scale = 1 - zDepth; mTrailEmitter.SpawnRate = (Math.Max( (float)Math.Pow(zDepth, 1.2f), 0f)) * .5f; mTrailEmitter.RegularEmissionType.Modifiers[0].FinalScale += scale * .5f; mTrailEmitter.MaxSpawnRadius += scale; } } /// /// The rocket pods on the outside of the boss's missile ring /// public class RocketPod { public int HitPoints; public Entity Entity; public Vector3 Offset; public float BeginRotation; public float EndRotation; public void GetHit(int damage, Vector3 hitFromPosition) { HitPoints -= damage; // Display the damage Rectangle proj = Space3D.Project(this.Entity.Position.X, this.Entity.Position.Y, hitFromPosition.Z, 1f, 1f, -1f); float scale = proj.Width * .005f; if (HitPoints <= 0) scale *= 5.0f; Emitter splosion = Sparx.LoadParticleEffect(@"Content\Particle Effects\SmallExplosion.spx"); splosion.Transform = Matrix.CreateScale(scale) * Matrix.CreateTranslation(proj.X, proj.Y, 0f); splosion.RenderScale = scale; Sparx.AddEmitter(splosion); } } /// /// 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 { public enum States { Approach, Idle, Boost, UnBoost, Turning, FarAttack, NearAttack, Dying }; protected MeteorMadness mGame; // Handle to the meteor madness game protected List mDebris; // Parts of the ship that fly off protected States mState; // State machine for AI logic protected float mStateTimer; // Some states are timed protected int mShields; // Must be destroyed before any other damage can occur protected int mMaxShields; // '' protected int mMaxHitPoints; // Used to calculate life percentage protected float mIdleZ; // Z-Depth of the ship in that state protected float mBoostZ; // '' (actually, the z-Depth to start decelerating but that doesn't really matter) protected float mShieldedDepth; // The boss actually gets smaller after its shields are gone protected List mRocketPods; // Each rocket pod on the ring protected bool mRingGone; // If the ring is still attached protected bool mNeedsToTurn; // True when the boss needs to turn around protected float mTurningTimer; // Counts down as the ship is turning around protected bool mVulnerable; // Whether the ship can be hit protected float mDeathTimer; // Counts down to from the player "killing" the boss to the game recognizing the boss is dead protected Vector3 mShake; // Shake when dying protected Emitter mDyingExplosions; // Emits death explosions protected Emitter[] mEngineFlares; // Engine flames protected Emitter mFlashEmitter; // MuzzleFlare protected Vector2[] mEngineFlareOffsets; // '' protected float mShotTimer; // Counts down to shooting protected int mLastRocket; // The last rocket that was fired protected Queue mNearAttackDelays; // Time between each near attack shot protected Queue mFarAttackDelays; // Time between each far attack protected bool mWaitingForDialog; // Wait until the user has read the encounter dialog to begin attacking public float ShieldLife { get { return (float)mShields / (float)mMaxShields; } } public float RocketPodLife { get { return (float)mRocketPods.Count / 24f; } } public float HullLife { get { return (float)mHitPoints / (float)mMaxHitPoints; } } /// /// Constructor /// /// In Space3D units /// In Space3D units /// In Space3D units /// How many hitpoints 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(MeteorMadness game, float width, float height, float depth, float idleZ, float boostZ, int shields, int hitPoints) : base(null, width, height, depth, hitPoints) { mGame = game; mSpace3DSize = new Vector3(width, height, depth); mShieldedDepth = depth; mIdleZ = idleZ; mPos.Z = boostZ * 10f; mBoostZ = boostZ; mMaxShields = shields; mMaxHitPoints = hitPoints; mShields = shields; mShake = Vector3.Zero; mAngVel = 0f; mWaitingForDialog = true; Texture2D bossSpriteSheet = TextureManager.Load(@"Content\MiniGames\MMBossSpriteSheet"); mSprite = new Animation(new Vector2(bossSpriteSheet.Width / 2, bossSpriteSheet.Height / 2)); 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(); Texture2D ringSpriteSheet = TextureManager.Load(@"Content\MiniGames\MMBossDebris"); Animation debrisAnim = new Animation(new Vector2(ringSpriteSheet.Width / 2, ringSpriteSheet.Height / 2)); debrisAnim.AddClip("Piece1", ringSpriteSheet, 1, 0, 0); mDebris.Add(new Entity(debrisAnim, this.Position, this.Size)); debrisAnim = new Animation(new Vector2(ringSpriteSheet.Width / 2, ringSpriteSheet.Height / 2)); debrisAnim.AddClip("Piece2", ringSpriteSheet, 1, 1, 0); mDebris.Add(new Entity(debrisAnim, this.Position, this.Size)); debrisAnim = new Animation(new Vector2(ringSpriteSheet.Width / 2, ringSpriteSheet.Height / 2)); debrisAnim.AddClip("Piece3", ringSpriteSheet, 1, 2, 0); mDebris.Add(new Entity(debrisAnim, this.Position, this.Size)); debrisAnim = new Animation(new Vector2(ringSpriteSheet.Width / 2, ringSpriteSheet.Height / 2)); debrisAnim.AddClip("Piece4", ringSpriteSheet, 1, 3, 0); mDebris.Add(new Entity(debrisAnim, this.Position, this.Size)); for(int i = 0; i < mDebris.Count; i++) mDebris[i].Visible = false; Texture2D[] rocketPods = new Texture2D[3]; rocketPods[0] = TextureManager.Load(@"Content\MiniGames\RocketPodA"); rocketPods[1] = TextureManager.Load(@"Content\MiniGames\RocketPodB"); rocketPods[2] = TextureManager.Load(@"Content\MiniGames\RocketPodC"); int numPods = 24; float rotationOffset = (float)(Math.PI * 2.0 / numPods * 0.5); mRocketPods = new List(); for (int i = 0; i < numPods; i++) { float thisArcRotation = (float)((float)i * Math.PI * 2.0 / (float)numPods); 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 + 0f; thisPositionOffset.Z = thisPositionOffset.Y * .1f; RocketPod newPod = new RocketPod(); newPod.Entity = new Entity(rocketPods[i % 3]); newPod.Entity.Position = mPos + thisPositionOffset; newPod.Offset = thisPositionOffset; float rocketWidth = .064f * mSpace3DSize.X; float rocketHeight = rocketWidth * newPod.Entity.Height / newPod.Entity.Width; newPod.Entity.Size = new Vector3(rocketWidth, rocketHeight, rocketHeight); game.GameBoard.AddEntity(newPod.Entity); newPod.HitPoints = 30; newPod.BeginRotation = thisArcRotation - rotationOffset; newPod.EndRotation = thisArcRotation + rotationOffset; mRocketPods.Add(newPod); } mFlashEmitter = Sparx.LoadParticleEffect(@"Content\Particle Effects\Flash.spx"); mFlashEmitter.RenderScale = .3f; mEngineFlares = new Emitter[3]; mEngineFlares[0] = Sparx.LoadParticleEffect(@"Content\Particle Effects\EngineFlare.spx"); mEngineFlares[1] = Sparx.LoadParticleEffect(@"Content\Particle Effects\EngineFlare.spx"); mEngineFlares[2] = Sparx.LoadParticleEffect(@"Content\Particle Effects\EngineFlare.spx"); ((Particle)(mEngineFlares[0].RegularEmissionType)).Height *= .5f; ((Particle)(mEngineFlares[1].RegularEmissionType)).Height *= .5f; ((Particle)(mEngineFlares[2].RegularEmissionType)).Height *= .5f; mEngineFlares[0].RenderScale = 0f; mEngineFlares[1].RenderScale = 0f; mEngineFlares[2].RenderScale = 0f; mEngineFlareOffsets = new Vector2[3]; mEngineFlareOffsets[0] = new Vector2(0f, -5f); mEngineFlareOffsets[1] = new Vector2(-35f, 15f); mEngineFlareOffsets[2] = new Vector2(30f, 15f); Sparx.AddEmitter(mEngineFlares[0]); Sparx.AddEmitter(mEngineFlares[1]); Sparx.AddEmitter(mEngineFlares[2]); mDyingExplosions = Sparx.LoadParticleEffect(@"Content\Particle Effects\MediumChainExplosions.spx"); mNearAttackDelays = new Queue(); mNearAttackDelays.Enqueue(.1f); mNearAttackDelays.Enqueue(.1f); mNearAttackDelays.Enqueue(.5f); mFarAttackDelays = new Queue(); for (int i = 0; i < 36; i++) mFarAttackDelays.Enqueue(.05f); mFarAttackDelays.Enqueue(3f); mState = States.Approach; mVulnerable = false; mNeedsToTurn = false; } /// /// Call this to allow the boss to start fighting /// public void Start() { mWaitingForDialog = false; } /// /// Returns a list of exposed rocketpods to test for collision /// /// public List GetVulnerableRocketPods() { if (mVulnerable && mShields <= 0) { return mRocketPods; } return new List(); } /// /// 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; // Shield particle effect mShieldEmitter.RegularEmissionType.LifeExpectancy = .1f; mShieldEmitter = Sparx.LoadParticleEffect(@"Content\Particle Effects\Shield.spx"); // Redden the shields to indicate that they are low float percentLeft = (float)mShields / (float)mMaxShields; Color newColor = new Color((byte)((255 * (1 - percentLeft))), (byte)((150 * (percentLeft))), (byte)((255 * (percentLeft)))); mShieldEmitter.RegularEmissionType.Modifiers[0].InitialTint = newColor; mShieldEmitter.RegularEmissionType.Modifiers[0].InitialOpacity = percentLeft; Sparx.AddEmitter(mShieldEmitter); } else if (mVulnerable && mRingGone) { if (mRocketPods.Count <= 0) { // Rocket pods are gone, so go ahead and damage the ship mHitPoints -= damage; // Display the damage Vector2 offset = new Vector2(hitFromPosition.X, hitFromPosition.Y); offset = Vector2.Normalize(offset); offset = Vector2.Multiply(offset, .5f); Rectangle proj = Space3D.Project(offset.X, offset.Y, hitFromPosition.Z, 1f, 1f, -1f); float scale = proj.Width * .0025f; Emitter splosion = Sparx.LoadParticleEffect(@"Content\Particle Effects\SmallExplosion.spx"); splosion.Transform = Matrix.CreateScale(.5f, 1f, 1f) * Matrix.CreateTranslation(proj.X, proj.Y, 0f); splosion.RenderScale = scale; Sparx.AddEmitter(splosion); if (mHitPoints <= 0) { mState = States.Dying; mDeathTimer = 5f; FadeOut(5f); proj = Space3D.Project(this.X, this.Y, this.Z, this.Width, this.Height, -1f); mDyingExplosions.Position2D = new Vector2(proj.X, proj.Y); Sparx.AddEmitter(mDyingExplosions); } } } } /// /// Update boss logic /// public override void Update() { float dT = AstroBaseApplication.GameManager.ElapsedSeconds; mStateTimer -= dT; mTurningTimer -= dT; mDeathTimer -= dT; mShotTimer -= dT; mVulnerable = true; if (mState == States.Approach || mState == States.UnBoost || mNeedsToTurn) mVulnerable = false; // Change collision size if shields are up or down if (mShields > 0) mSpace3DSize.Z = mShieldedDepth * .5f; else mSpace3DSize.Z = mShieldedDepth * .01f; // Move the shields Rectangle proj = Space3D.Project(this.X, this.Y, this.Z, this.Width, this.Height, -1f); float scale = proj.Width * .009f; mShieldEmitter.RenderScale = scale; mShieldEmitter.Transform = Matrix.CreateScale(scale) * Matrix.CreateTranslation(proj.X, proj.Y, 0f); // Move the engine flares scale = proj.Width * .003f; for (int i = 0; i < 3; i++) { mEngineFlares[i].RenderScale = scale; mEngineFlares[i].Transform = Matrix.CreateScale(scale) * Matrix.CreateTranslation(proj.X + scale * mEngineFlareOffsets[i].X, proj.Y + scale * mEngineFlareOffsets[i].Y, 0f); } // Destroy ring if necessary if (mRocketPods.Count == 0 && !mRingGone) { mRingGone = true; mNeedsToTurn = true; mVulnerable = false; mSprite.Play("Phase2"); // Display the damage scale = 5.0f; Emitter splosion = Sparx.LoadParticleEffect(@"Content\Particle Effects\SmallExplosion.spx"); splosion.Transform = Matrix.CreateScale(scale) * Matrix.CreateTranslation(proj.X, proj.Y, 0f); splosion.RenderScale = scale; splosion.MinSpawnRadius = 15; splosion.MaxSpawnRadius = 15; splosion.DieAfter *= 10; Sparx.AddEmitter(splosion); 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(this.Size, .5f); mDebris[i].Position += this.Position; mDebris[i].Velocity = mDebris[i].Position; mDebris[i].dX *= GenericBaseApplication.GameManager.RandomNumber(.01f, 1.0f); mDebris[i].dY *= GenericBaseApplication.GameManager.RandomNumber(.01f, 1.0f); mDebris[i].dZ = GenericBaseApplication.GameManager.RandomNumber(-.5f, -.05f); mDebris[i].Visible = true; mDebris[i].FadeOut(4f); mGame.GameBoard.AddEntity(mDebris[i]); } } for (int i = mDebris.Count - 1; i >= 0; i--) { mDebris[i].Update(); mDebris[i].Velocity *= 1.02f; if (mDebris[i].Z < mGame.GameBoard.MinZ) mDebris.RemoveAt(i); } // Perform logic based on state switch (mState) { 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; 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) { mState = States.Boost; mStateTimer = GenericBaseApplication.GameManager.RandomNumber(3f, 6f); } // GOTO: boost, close attack break; case States.Boost: // Speed away from the player if (mPos.Z < mBoostZ) { // Speed up mVel.Z += .1f; for (int i = 0; i < 3; i++) mEngineFlares[i].RenderScale *= 2.5f; } else { // Slow down if (mVel.Z > 0) mVel.Z *= .5f; if (mVel.Z < .1f) mVel.Z = 0; for (int i = 0; i < 3; i++) mEngineFlares[i].RenderScale *= 1.5f; } // GOTO: Far attack, Turn around if (mStateTimer < 0) { if (mNeedsToTurn) { mTurningTimer = 1f; FadeOut(1f); mNeedsToTurn = false; mState = States.Turning; mEngineFlares[0].Alive = false; mEngineFlares[1].Alive = false; mEngineFlares[2].Alive = false; } else { mStateTimer = GenericBaseApplication.GameManager.RandomNumber(6f, 12f); mState = States.FarAttack; } } break; 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.Entity.Position, (curRocket.BeginRotation + curRocket.EndRotation) / 2f - MathHelper.PiOver2, this.Z, mGame.GameBoard.MinZ); mGame.Projectiles.Add(bossRocket); mGame.GameBoard.AddEntity(bossRocket); mShotTimer = mFarAttackDelays.Dequeue(); mFarAttackDelays.Enqueue(mShotTimer); Rectangle screenCoords = Space3D.Project(bossRocket.X, bossRocket.Y, bossRocket.Z, bossRocket.Width, bossRocket.Height, 0f); Emitter flash = (Emitter)mFlashEmitter.Clone(); flash.RenderScale = .05f; flash.Position2D = new Vector2(screenCoords.X, screenCoords.Y); Sparx.AddEmitter(flash); } for (int i = 0; i < 3; i++) mEngineFlares[i].RenderScale *= 1.5f; // GOTO: Unboost if (mStateTimer < 0) { // Let the player catch up and then resume idle mState = States.UnBoost; } break; 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 = (float)(Math.PI / .96); mState = States.NearAttack; } else { mState = States.Idle; mStateTimer = GenericBaseApplication.GameManager.RandomNumber(10f, 15f); } } break; case States.Turning: // Turn around and face the player // GOTO: UnBoost if (mTurningTimer <= 0) { mSprite.Play("Phase3"); FadeIn(1f); mState = States.UnBoost; } break; case States.NearAttack: // Attack the player relentlessly! if (mTurningTimer < 0) { mAngVel *= -1f; mTurningTimer = GenericBaseApplication.GameManager.RandomNumber(5f, 10f); } if (mShotTimer < 0) { mShotTimer = mNearAttackDelays.Dequeue(); mNearAttackDelays.Enqueue(mShotTimer); Vector3 direction = new Vector3(.5f * (float)Math.Cos(mRot + MathHelper.PiOver2), .5f * (float)Math.Sin(mRot + MathHelper.PiOver2), this.Position.Z); // Fire a 5-shot spread for (int i = 0; i < 5; i++) { BossLaser shot = new BossLaser( direction, this.Position.Z, mGame.GameBoard.MinZ); shot.dX += GenericBaseApplication.GameManager.RandomNumber(-.1f, .1f); shot.dY += GenericBaseApplication.GameManager.RandomNumber(-.1f, .1f); shot.Z -= i * .5f; mGame.Projectiles.Add(shot); mGame.GameBoard.AddEntity(shot); // Put a flash on the last shot (looks the best at taht location) if (i == 4) { Rectangle screenCoords = Space3D.Project(shot.X, shot.Y, shot.Z, shot.Width, shot.Height, 0f); Emitter flash = (Emitter)mFlashEmitter.Clone(); flash.Position2D = new Vector2(screenCoords.X, screenCoords.Y); Sparx.AddEmitter(flash); } } } break; case States.Dying: // Shake, spin and die mShake = new Vector3( .05f * (float)Math.Cos(GenericBaseApplication.GameManager.TotalSeconds * 25f), .01f * (float)Math.Sin(GenericBaseApplication.GameManager.TotalSeconds * 5f), 0f); mAngVel *= 1.01f; // GOTO: Nothing if (mDeathTimer <= 0) { mDyingExplosions.Alive = false; mAlive = false; } break; } this.Position += mShake; base.Update(); // Update rocket pods List deathRow = new List(); foreach (RocketPod rocketPod in mRocketPods) { rocketPod.Entity.Update(); rocketPod.Entity.Position = mPos + rocketPod.Offset; if (rocketPod.HitPoints <= 0) { mGame.GameBoard.RemoveEntity(rocketPod.Entity); deathRow.Add(rocketPod); } } foreach (RocketPod corpse in deathRow) mRocketPods.Remove(corpse); } } }