#region File Description
//-----------------------------------------------------------------------------
// CombatEngine.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion
#region Using Statements
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using RolePlayingGameData;
#endregion
namespace RolePlaying
{
///
/// The runtime execution engine for the combat system.
///
class CombatEngine
{
#region Singleton
///
/// The singleton of the combat engine.
///
private static CombatEngine singleton = null;
///
/// Check to see if there is a combat going on, and throw an exception if not.
///
private static void CheckSingleton()
{
if (singleton == null)
{
throw new InvalidOperationException(
"There is no active combat at this time.");
}
}
#endregion
#region State
///
/// If true, the combat engine is active and the user is in combat.
///
public static bool IsActive
{
get { return (singleton != null); }
}
///
/// If true, it is currently the players' turn.
///
private bool isPlayersTurn;
///
/// If true, it is currently the players' turn.
///
public static bool IsPlayersTurn
{
get
{
CheckSingleton();
return singleton.isPlayersTurn;
}
}
#endregion
#region Rewards
///
/// The fixed combat used to generate this fight, if any.
///
///
/// Used for rewards. Null means it was a random fight with no special rewards.
///
private MapEntry fixedCombatEntry;
///
/// The fixed combat used to generate this fight, if any.
///
///
/// Used for rewards. Null means it was a random fight with no special rewards.
///
public static MapEntry FixedCombatEntry
{
get { return (singleton == null ? null : singleton.fixedCombatEntry); }
}
#endregion
#region Players
///
/// The players involved in the current combat.
///
private List players = null;
///
/// The players involved in the current combat.
///
public static List Players
{
get
{
CheckSingleton();
return singleton.players;
}
}
private int highlightedPlayer;
///
/// The positions of the players on screen.
///
private static readonly Vector2[] PlayerPositions = new Vector2[5]
{
new Vector2(850f, 345f),
new Vector2(980f, 260f),
new Vector2(940f, 440f),
new Vector2(1100f, 200f),
new Vector2(1100f, 490f)
};
///
/// Start the given player's combat turn.
///
private void BeginPlayerTurn(CombatantPlayer player)
{
// check the parameter
if (player == null)
{
throw new ArgumentNullException("player");
}
// set the highlight sprite
highlightedCombatant = player;
primaryTargetedCombatant = null;
secondaryTargetedCombatants.Clear();
Session.Hud.ActionText = "Choose an Action";
}
///
/// Begin the players' turn in this combat round.
///
private void BeginPlayersTurn()
{
// set the player-turn
isPlayersTurn = true;
// reset each player for the next combat turn
foreach (CombatantPlayer player in players)
{
// reset the animation of living players
if (!player.IsDeadOrDying)
{
player.State = Character.CharacterState.Idle;
}
// reset the turn-taken flag
player.IsTurnTaken = false;
// clear the combat action
player.CombatAction = null;
// advance each player
player.AdvanceRound();
}
// set the action text on the HUD
Session.Hud.ActionText = "Your Party's Turn";
// find the first player who is alive
highlightedPlayer = 0;
CombatantPlayer firstPlayer = players[highlightedPlayer];
while (firstPlayer.IsTurnTaken || firstPlayer.IsDeadOrDying)
{
highlightedPlayer = (highlightedPlayer + 1) % players.Count;
firstPlayer = players[highlightedPlayer];
}
// start the first player's turn
BeginPlayerTurn(firstPlayer);
}
///
/// Check for whether all players have taken their turn.
///
private bool IsPlayersTurnComplete
{
get
{
return players.TrueForAll(delegate(CombatantPlayer player)
{
return (player.IsTurnTaken || player.IsDeadOrDying);
});
}
}
///
/// Check for whether the players have been wiped out and defeated.
///
private bool ArePlayersDefeated
{
get
{
return players.TrueForAll(delegate(CombatantPlayer player)
{
return (player.State == Character.CharacterState.Dead);
});
}
}
///
/// Retrieves the first living player, if any.
///
private CombatantPlayer FirstPlayerTarget
{
get
{
// if there are no living players, then this is moot
if (ArePlayersDefeated)
{
return null;
}
int playerIndex = 0;
while ((playerIndex < players.Count) &&
players[playerIndex].IsDeadOrDying)
{
playerIndex++;
}
return players[playerIndex];
}
}
#endregion
#region Monsters
///
/// The monsters involved in the current combat.
///
private List monsters = null;
///
/// The monsters involved in the current combat.
///
public static List Monsters
{
get
{
CheckSingleton();
return singleton.monsters;
}
}
///
/// The positions of the monsters on the screen.
///
private static readonly Vector2[] MonsterPositions = new Vector2[5]
{
new Vector2(480f, 345f),
new Vector2(345f, 260f),
new Vector2(370f, 440f),
new Vector2(225f, 200f),
new Vector2(225f, 490f)
};
///
/// Start the given player's combat turn.
///
private void BeginMonsterTurn(CombatantMonster monster)
{
// if it's null, find a random living monster who has yet to take their turn
if (monster == null)
{
// don't bother if all monsters have finished
if (IsMonstersTurnComplete)
{
return;
}
// pick random living monsters who haven't taken their turn
do
{
monster = monsters[Session.Random.Next(monsters.Count)];
}
while (monster.IsTurnTaken || monster.IsDeadOrDying);
}
// set the highlight sprite
highlightedCombatant = monster;
primaryTargetedCombatant = null;
secondaryTargetedCombatants.Clear();
// choose the action immediate
monster.CombatAction = monster.ArtificialIntelligence.ChooseAction();
}
///
/// Begin the monsters' turn in this combat round.
///
private void BeginMonstersTurn()
{
// set the monster-turn
isPlayersTurn = false;
// reset each monster for the next combat turn
foreach (CombatantMonster monster in monsters)
{
// reset the animations back to idle
monster.Character.State = Character.CharacterState.Idle;
// reset the turn-taken flag
monster.IsTurnTaken = false;
// clear the combat action
monster.CombatAction = null;
// advance the combatants
monster.AdvanceRound();
}
// set the action text on the HUD
Session.Hud.ActionText = "Enemy Party's Turn";
// start a Session.Random monster's turn
BeginMonsterTurn(null);
}
///
/// Check for whether all monsters have taken their turn.
///
private bool IsMonstersTurnComplete
{
get
{
return monsters.TrueForAll(delegate(CombatantMonster monster)
{
return (monster.IsTurnTaken || monster.IsDeadOrDying);
});
}
}
///
/// Check for whether the monsters have been wiped out and defeated.
///
private bool AreMonstersDefeated
{
get
{
return monsters.TrueForAll(delegate(CombatantMonster monster)
{
return (monster.State == Character.CharacterState.Dead);
});
}
}
///
/// Retrieves the first living monster, if any.
///
private CombatantMonster FirstMonsterTarget
{
get
{
// if there are no living monsters, then this is moot
if (AreMonstersDefeated)
{
return null;
}
int monsterIndex = 0;
while ((monsterIndex < monsters.Count) &&
monsters[monsterIndex].IsDeadOrDying)
{
monsterIndex++;
}
return monsters[monsterIndex];
}
}
#endregion
#region Targeting
///
/// The currently highlighted player, if any.
///
private Combatant highlightedCombatant;
///
/// The currently highlighted player, if any.
///
public static Combatant HighlightedCombatant
{
get
{
CheckSingleton();
return singleton.highlightedCombatant;
}
}
///
/// The current primary target, if any.
///
private Combatant primaryTargetedCombatant;
///
/// The current primary target, if any.
///
public static Combatant PrimaryTargetedCombatant
{
get
{
CheckSingleton();
return singleton.primaryTargetedCombatant;
}
}
///
/// The current secondary targets, if any.
///
private List secondaryTargetedCombatants = new List();
///
/// The current secondary targets, if any.
///
public static List SecondaryTargetedCombatants
{
get
{
CheckSingleton();
return singleton.secondaryTargetedCombatants;
}
}
///
/// Retrieves the first living enemy, if any.
///
public static Combatant FirstEnemyTarget
{
get
{
CheckSingleton();
if (IsPlayersTurn)
{
return singleton.FirstMonsterTarget;
}
else
{
return singleton.FirstPlayerTarget;
}
}
}
///
/// Retrieves the first living ally, if any.
///
public static Combatant FirstAllyTarget
{
get
{
CheckSingleton();
if (IsPlayersTurn)
{
return singleton.FirstPlayerTarget;
}
else
{
return singleton.FirstMonsterTarget;
}
}
}
///
/// Set the primary and any secondary targets.
///
/// The desired primary target.
///
/// The number of simultaneous, adjacent targets affected by this spell.
///
private void SetTargets(Combatant primaryTarget, int adjacentTargets)
{
// set the primary target
this.primaryTargetedCombatant = primaryTarget;
// set any secondary targets
this.secondaryTargetedCombatants.Clear();
if ((primaryTarget != null) && (adjacentTargets > 0))
{
// find out which side is targeted
bool isPlayerTarget = primaryTarget is CombatantPlayer;
// find the index
int primaryTargetIndex = 0;
if (isPlayerTarget)
{
primaryTargetIndex = players.FindIndex(
delegate(CombatantPlayer player)
{
return (player == primaryTarget);
});
}
else
{
primaryTargetIndex = monsters.FindIndex(
delegate(CombatantMonster monster)
{
return (monster == primaryTarget);
});
}
// add the surrounding indices
for (int i = 1; i <= adjacentTargets; i++)
{
int leftIndex = primaryTargetIndex - i;
if (leftIndex >= 0)
{
secondaryTargetedCombatants.Add(
isPlayerTarget ? players[leftIndex] as Combatant :
monsters[leftIndex] as Combatant);
}
int rightIndex = primaryTargetIndex + i;
if (rightIndex < (isPlayerTarget ? players.Count : monsters.Count))
{
secondaryTargetedCombatants.Add(
isPlayerTarget ? players[rightIndex] as Combatant :
monsters[rightIndex] as Combatant);
}
}
}
}
#endregion
#region Damage Sprites
///
/// A combat effect sprite, typically used for damage or healing numbers.
///
private class CombatEffect
{
#region Position
///
/// The starting position of the effect on the screen.
///
public Vector2 OriginalPosition;
///
/// The current position of the effect on the screen.
///
protected Vector2 position;
///
/// The current position of the effect on the screen.
///
public Vector2 Position
{
get { return position; }
}
#endregion
#region Text
///
/// The text that appears on top of the effect.
///
protected string text = String.Empty;
///
/// The text that appears on top of the effect.
///
public string Text
{
get { return text; }
set
{
text = value;
// recalculate the origin
if (String.IsNullOrEmpty(text))
{
textOrigin = Vector2.Zero;
}
else
{
Vector2 textSize = Fonts.DamageFont.MeasureString(text);
textOrigin = new Vector2(
(float)Math.Ceiling(textSize.X / 2f),
(float)Math.Ceiling(textSize.Y / 2f));
}
}
}
///
/// The drawing origin of the text on the effect.
///
private Vector2 textOrigin = Vector2.Zero;
#endregion
#region Rise Animation
///
/// The speed at which the effect rises on the screen.
///
const int risePerSecond = 100;
///
/// The amount which the effect rises on the screen.
///
const int riseMaximum = 80;
///
/// The amount which the effect has already risen on the screen.
///
public float Rise = 0;
///
/// If true, the effect has finished rising.
///
private bool isRiseComplete = false;
///
/// If true, the effect has finished rising.
///
public bool IsRiseComplete
{
get { return isRiseComplete; }
}
#endregion
#region Updating
///
/// Updates the combat effect.
///
///
/// The number of seconds elapsed since the last update.
///
public virtual void Update(float elapsedSeconds)
{
if (!isRiseComplete)
{
Rise += ((float)risePerSecond * elapsedSeconds);
if (Rise > riseMaximum)
{
Rise = riseMaximum;
isRiseComplete = true;
}
position = new Vector2(
OriginalPosition.X,
OriginalPosition.Y - Rise);
}
}
#endregion
#region Drawing
///
/// Draw the combat effect.
///
/// The SpriteBatch used to draw.
/// The texture for the effect.
public virtual void Draw(SpriteBatch spriteBatch, Texture2D texture)
{
// check the parameter
if (spriteBatch == null)
{
return;
}
// draw the texture
if (texture != null)
{
spriteBatch.Draw(texture, position, null, Color.White, 0f,
new Vector2(texture.Width / 2, texture.Height / 2), 1f,
SpriteEffects.None, 0.3f * (float)Rise / 200f);
}
// draw the text
if (!String.IsNullOrEmpty(Text))
{
spriteBatch.DrawString(Fonts.DamageFont, text, position,
Color.White, 0f, new Vector2(textOrigin.X, textOrigin.Y), 1f,
SpriteEffects.None, 0.2f * (float)Rise / 200f);
}
}
#endregion
}
#region Damage Effects
///
/// The sprite texture for all damage combat effects.
///
private Texture2D damageCombatEffectTexture;
///
/// All current damage combat effects.
///
private List damageCombatEffects = new List();
///
/// Adds a new damage combat effect to the scene.
///
/// The position that the effect starts at.
/// The damage statistics.
public static void AddNewDamageEffects(Vector2 position,
StatisticsValue damage)
{
int startingRise = 0;
CheckSingleton();
if (damage.HealthPoints != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "HP\n" + damage.HealthPoints.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.damageCombatEffects.Add(combatEffect);
}
if (damage.MagicPoints != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "MP\n" + damage.MagicPoints.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.damageCombatEffects.Add(combatEffect);
}
if (damage.PhysicalOffense != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "PO\n" + damage.PhysicalOffense.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.damageCombatEffects.Add(combatEffect);
}
if (damage.PhysicalDefense != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "PD\n" + damage.PhysicalDefense.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.damageCombatEffects.Add(combatEffect);
}
if (damage.MagicalOffense != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "MO\n" + damage.MagicalOffense.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.damageCombatEffects.Add(combatEffect);
}
if (damage.MagicalDefense != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "MD\n" + damage.MagicalDefense.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.damageCombatEffects.Add(combatEffect);
}
}
#endregion
#region Healing Combat Effects
///
/// The sprite texture for all healing combat effects.
///
private Texture2D healingCombatEffectTexture;
///
/// All current healing combat effects.
///
private List healingCombatEffects = new List();
///
/// Adds a new healing combat effect to the scene.
///
/// The position that the effect starts at.
/// The healing statistics.
public static void AddNewHealingEffects(Vector2 position,
StatisticsValue healing)
{
int startingRise = 0;
CheckSingleton();
if (healing.HealthPoints != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "HP\n" + healing.HealthPoints.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.healingCombatEffects.Add(combatEffect);
}
if (healing.MagicPoints != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "MP\n" + healing.MagicPoints.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.healingCombatEffects.Add(combatEffect);
}
if (healing.PhysicalOffense != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "PO\n" + healing.PhysicalOffense.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.healingCombatEffects.Add(combatEffect);
}
if (healing.PhysicalDefense != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "PD\n" + healing.PhysicalDefense.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.healingCombatEffects.Add(combatEffect);
}
if (healing.MagicalOffense != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "MO\n" + healing.MagicalOffense.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.healingCombatEffects.Add(combatEffect);
}
if (healing.MagicalDefense != 0)
{
CombatEffect combatEffect = new CombatEffect();
combatEffect.OriginalPosition = position;
combatEffect.Text = "MD\n" + healing.MagicalDefense.ToString();
combatEffect.Rise = startingRise;
startingRise -= 5;
singleton.healingCombatEffects.Add(combatEffect);
}
}
#endregion
///
/// Load the graphics data for the combat effect sprites.
///
private void CreateCombatEffectSprites()
{
ContentManager content = Session.ScreenManager.Game.Content;
damageCombatEffectTexture =
content.Load(@"Textures\Combat\DamageIcon");
healingCombatEffectTexture =
content.Load(@"Textures\Combat\HealingIcon");
}
///
/// Draw all combat effect sprites.
///
private void DrawCombatEffects(GameTime gameTime)
{
float elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
SpriteBatch spriteBatch = Session.ScreenManager.SpriteBatch;
// update all effects
foreach (CombatEffect combatEffect in damageCombatEffects)
{
combatEffect.Update(elapsedSeconds);
}
foreach (CombatEffect combatEffect in healingCombatEffects)
{
combatEffect.Update(elapsedSeconds);
}
// draw the damage effects
if (damageCombatEffectTexture != null)
{
foreach (CombatEffect combatEffect in damageCombatEffects)
{
combatEffect.Draw(spriteBatch, damageCombatEffectTexture);
}
}
// draw the healing effects
if (healingCombatEffectTexture != null)
{
foreach (CombatEffect combatEffect in healingCombatEffects)
{
combatEffect.Draw(spriteBatch, healingCombatEffectTexture);
}
}
// remove all complete effects
Predicate removeCompleteEffects =
delegate(CombatEffect combatEffect)
{
return combatEffect.IsRiseComplete;
};
damageCombatEffects.RemoveAll(removeCompleteEffects);
healingCombatEffects.RemoveAll(removeCompleteEffects);
}
#endregion
#region Selection Sprites
///
/// The animating sprite that draws over the highlighted character.
///
private AnimatingSprite highlightForegroundSprite = new AnimatingSprite();
///
/// The animating sprite that draws behind the highlighted character.
///
private AnimatingSprite highlightBackgroundSprite = new AnimatingSprite();
///
/// The animating sprite that draws behind the primary target character.
///
private AnimatingSprite primaryTargetSprite = new AnimatingSprite();
///
/// The animating sprite that draws behind any secondary target characters.
///
private AnimatingSprite secondaryTargetSprite = new AnimatingSprite();
///
/// Create the selection sprite objects.
///
private void CreateSelectionSprites()
{
ContentManager content = Session.ScreenManager.Game.Content;
Point frameDimensions = new Point(76, 58);
highlightForegroundSprite.FramesPerRow = 6;
highlightForegroundSprite.FrameDimensions = frameDimensions;
highlightForegroundSprite.AddAnimation(
new Animation("Selection", 1, 4, 100, true));
highlightForegroundSprite.PlayAnimation(0);
highlightForegroundSprite.SourceOffset =
new Vector2(frameDimensions.X / 2f, 40f);
highlightForegroundSprite.Texture =
content.Load(@"Textures\Combat\TilesheetSprangles");
frameDimensions = new Point(102, 54);
highlightBackgroundSprite.FramesPerRow = 4;
highlightBackgroundSprite.FrameDimensions = frameDimensions;
highlightBackgroundSprite.AddAnimation(
new Animation("Selection", 1, 4, 100, true));
highlightBackgroundSprite.PlayAnimation(0);
highlightBackgroundSprite.SourceOffset =
new Vector2(frameDimensions.X / 2f, frameDimensions.Y / 2f);
highlightBackgroundSprite.Texture =
content.Load(@"Textures\Combat\CharSelectionRing");
primaryTargetSprite.FramesPerRow = 4;
primaryTargetSprite.FrameDimensions = frameDimensions;
primaryTargetSprite.AddAnimation(
new Animation("Selection", 1, 4, 100, true));
primaryTargetSprite.PlayAnimation(0);
primaryTargetSprite.SourceOffset =
new Vector2(frameDimensions.X / 2f, frameDimensions.Y / 2f);
primaryTargetSprite.Texture =
content.Load(@"Textures\Combat\Target1SelectionRing");
secondaryTargetSprite.FramesPerRow = 4;
secondaryTargetSprite.FrameDimensions = frameDimensions;
secondaryTargetSprite.AddAnimation(
new Animation("Selection", 1, 4, 100, true));
secondaryTargetSprite.PlayAnimation(0);
secondaryTargetSprite.SourceOffset =
new Vector2(frameDimensions.X / 2f, frameDimensions.Y / 2f);
secondaryTargetSprite.Texture =
content.Load(@"Textures\Combat\Target2SelectionRing");
}
///
/// Draw the highlight sprites.
///
private void DrawSelectionSprites(GameTime gameTime)
{
SpriteBatch spriteBatch = Session.ScreenManager.SpriteBatch;
Viewport viewport = Session.ScreenManager.GraphicsDevice.Viewport;
// update the animations
float elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
highlightForegroundSprite.UpdateAnimation(elapsedSeconds);
highlightBackgroundSprite.UpdateAnimation(elapsedSeconds);
primaryTargetSprite.UpdateAnimation(elapsedSeconds);
secondaryTargetSprite.UpdateAnimation(elapsedSeconds);
// draw the highlighted-player sprite, if any
if (highlightedCombatant != null)
{
highlightBackgroundSprite.Draw(spriteBatch,
highlightedCombatant.Position,
1f - (highlightedCombatant.Position.Y - 1) / viewport.Height);
highlightForegroundSprite.Draw(spriteBatch,
highlightedCombatant.Position,
1f - (highlightedCombatant.Position.Y + 1) / viewport.Height);
}
// draw the primary target sprite and name, if any
if (primaryTargetedCombatant != null)
{
primaryTargetSprite.Draw(spriteBatch,
primaryTargetedCombatant.Position,
1f - (primaryTargetedCombatant.Position.Y - 1) / viewport.Height);
if (primaryTargetedCombatant.Character is Monster)
{
Fonts.DrawCenteredText(spriteBatch, Fonts.DamageFont,
#if DEBUG
primaryTargetedCombatant.Character.Name + "\n" +
primaryTargetedCombatant.Statistics.HealthPoints + "/" +
primaryTargetedCombatant.Character.CharacterStatistics.HealthPoints,
#else
primaryTargetedCombatant.Character.Name,
#endif
primaryTargetedCombatant.Position + new Vector2(0f, 42f),
Color.White);
}
}
// draw the secondary target sprites on live enemies, if any
foreach (Combatant combatant in secondaryTargetedCombatants)
{
if (combatant.IsDeadOrDying)
{
continue;
}
secondaryTargetSprite.Draw(spriteBatch,
combatant.Position,
1f - (combatant.Position.Y - 1) / viewport.Height);
if (combatant.Character is Monster)
{
Fonts.DrawCenteredText(spriteBatch, Fonts.DamageFont,
#if DEBUG
combatant.Character.Name + "\n" +
combatant.Statistics.HealthPoints + "/" +
combatant.Character.CharacterStatistics.HealthPoints,
#else
combatant.Character.Name,
#endif
combatant.Position + new Vector2(0f, 42f), Color.White);
}
}
}
#endregion
#region Delays
///
/// Varieties of delays that are interspersed throughout the combat flow.
///
private enum DelayType
{
///
/// No delay at this time.
///
NoDelay,
///
/// Delay at the start of combat.
///
StartCombat,
///
/// Delay when one side turn's ends before the other side begins.
///
EndRound,
///
/// Delay at the end of a character's turn before the next one begins.
///
EndCharacterTurn,
///
/// Delay before a flee is attempted.
///
FleeAttempt,
///
/// Delay when the party has fled from combat before combat ends.
///
FleeSuccessful,
}
///
/// The current delay, if any (otherwise NoDelay).
///
private DelayType delayType = DelayType.NoDelay;
///
/// Returns true if the combat engine is delaying for any reason.
///
public static bool IsDelaying
{
get
{
return (singleton == null ? false :
singleton.delayType != DelayType.NoDelay);
}
}
///
/// The duration for all kinds of delays, in milliseconds.
///
private const int totalDelay = 1000;
///
/// The duration of the delay so far.
///
private int currentDelay = 0;
///
/// Update any delays in the combat system.
///
///
/// This function may cause combat to end, setting the singleton to null.
///
private void UpdateDelay(int elapsedMilliseconds)
{
if (delayType == DelayType.NoDelay)
{
return;
}
// increment the delay
currentDelay += elapsedMilliseconds;
// if the delay is ongoing, then we're done
if (currentDelay < totalDelay)
{
return;
}
currentDelay = 0;
// the delay has ended, so the operation implied by the DelayType happens
switch (delayType)
{
case DelayType.StartCombat:
// determine who goes first and start combat
int whoseTurn = Session.Random.Next(2);
if (whoseTurn == 0)
{
BeginPlayersTurn();
}
else
{
BeginMonstersTurn();
}
delayType = DelayType.NoDelay;
break;
case DelayType.EndCharacterTurn:
if (IsPlayersTurn)
{
// check to see if the players' turn is complete
if (IsPlayersTurnComplete)
{
delayType = DelayType.EndRound;
break;
}
// find the next player
int highlightedIndex = players.FindIndex(
delegate(CombatantPlayer player)
{
return (player ==
highlightedCombatant as CombatantPlayer);
});
int nextIndex = (highlightedIndex + 1) % players.Count;
while (players[nextIndex].IsDeadOrDying ||
players[nextIndex].IsTurnTaken)
{
nextIndex = (nextIndex + 1) % players.Count;
}
BeginPlayerTurn(players[nextIndex]);
}
else
{
// check to see if the monsters' turn is complete
if (IsMonstersTurnComplete)
{
delayType = DelayType.EndRound;
break;
}
// find the next monster
BeginMonsterTurn(null);
}
delayType = DelayType.NoDelay;
break;
case DelayType.EndRound:
// check for turn completion
if (IsPlayersTurn && IsPlayersTurnComplete)
{
BeginMonstersTurn();
}
else if (!IsPlayersTurn && IsMonstersTurnComplete)
{
BeginPlayersTurn();
}
delayType = DelayType.NoDelay;
break;
case DelayType.FleeAttempt:
if (fleeThreshold <= 0)
{
delayType = DelayType.EndCharacterTurn;
Session.Hud.ActionText = "This Fight Cannot Be Escaped...";
if (highlightedCombatant != null)
{
highlightedCombatant.IsTurnTaken = true;
}
}
else if (CalculateFleeAttempt())
{
delayType = DelayType.FleeSuccessful;
Session.Hud.ActionText = "Your Party Has Fled!";
}
else
{
delayType = DelayType.EndCharacterTurn;
Session.Hud.ActionText = "Your Party Failed to Escape!";
if (highlightedCombatant != null)
{
highlightedCombatant.IsTurnTaken = true;
}
}
break;
case DelayType.FleeSuccessful:
EndCombat(CombatEndingState.Fled);
delayType = DelayType.NoDelay;
break;
}
}
#endregion
#region Starting Combat
///
/// Generates a list of CombatantPlayer objects from the party members.
///
private static List GenerateCombatantsFromParty()
{
List generatedPlayers = new List();
foreach (Player player in Session.Party.Players)
{
if (generatedPlayers.Count <= PlayerPositions.Length)
{
generatedPlayers.Add(new CombatantPlayer(player));
}
}
return generatedPlayers;
}
///
/// Start a new combat from the given FixedCombat object.
///
public static void StartNewCombat(MapEntry fixedCombatEntry)
{
// check the parameter
if (fixedCombatEntry == null)
{
throw new ArgumentNullException("fixedCombatEntry");
}
FixedCombat fixedCombat = fixedCombatEntry.Content;
if (fixedCombat == null)
{
throw new ArgumentException("fixedCombatEntry has no content.");
}
// generate the monster combatant list
List generatedMonsters = new List();
foreach (ContentEntry entry in fixedCombat.Entries)
{
for (int i = 0; i < entry.Count; i++)
{
generatedMonsters.Add(
new CombatantMonster(entry.Content));
}
}
// randomize the list of monsters
List randomizedMonsters = new List();
while ((generatedMonsters.Count > 0) &&
(randomizedMonsters.Count <= MonsterPositions.Length))
{
int index = Session.Random.Next(generatedMonsters.Count);
randomizedMonsters.Add(generatedMonsters[index]);
generatedMonsters.RemoveAt(index);
}
// start the combat
StartNewCombat(GenerateCombatantsFromParty(), randomizedMonsters, 0);
singleton.fixedCombatEntry = fixedCombatEntry;
}
///
/// Start a new combat from the given RandomCombat object.
///
public static void StartNewCombat(RandomCombat randomCombat)
{
// check the parameter
if (randomCombat == null)
{
throw new ArgumentNullException("randomCombat");
}
// determine how many monsters will be in the combat
int monsterCount =
randomCombat.MonsterCountRange.GenerateValue(Session.Random);
// determine the total probability
int totalWeight = 0;
foreach (WeightedContentEntry entry in randomCombat.Entries)
{
totalWeight += entry.Weight;
}
// generate each monster
List generatedMonsters = new List();
for (int i = 0; i < monsterCount; i++)
{
int monsterChoice = Session.Random.Next(totalWeight);
foreach (WeightedContentEntry entry in randomCombat.Entries)
{
if (monsterChoice < entry.Weight)
{
generatedMonsters.Add(
new CombatantMonster(entry.Content));
break;
}
else
{
monsterChoice -= entry.Weight;
}
}
}
// randomize the list of monsters
List randomizedMonsters = new List();
while ((generatedMonsters.Count > 0) &&
(randomizedMonsters.Count <= MonsterPositions.Length))
{
int index = Session.Random.Next(generatedMonsters.Count);
randomizedMonsters.Add(generatedMonsters[index]);
generatedMonsters.RemoveAt(index);
}
// start the combat
StartNewCombat(GenerateCombatantsFromParty(), randomizedMonsters,
randomCombat.FleeProbability);
}
///
/// Start a new combat between the party and a group of monsters.
///
/// The player combatants.
/// The monster combatants.
/// The odds of success when fleeing.
private static void StartNewCombat(List players,
List monsters, int fleeThreshold)
{
// check if we are already in combat
if (singleton != null)
{
throw new InvalidOperationException(
"There can only be one combat at a time.");
}
// create the new CombatEngine object
singleton = new CombatEngine(players, monsters, fleeThreshold);
}
///
/// Construct a new CombatEngine object.
///
/// The player combatants.
/// The monster combatants.
/// The odds of success when fleeing.
private CombatEngine(List players,
List monsters, int fleeThreshold)
{
// check the parameters
if ((players == null) || (players.Count <= 0) ||
(players.Count > PlayerPositions.Length))
{
throw new ArgumentException("players");
}
if ((monsters == null) || (monsters.Count <= 0) ||
(monsters.Count > MonsterPositions.Length))
{
throw new ArgumentException("monsters");
}
// assign the parameters
this.players = players;
this.monsters = monsters;
this.fleeThreshold = fleeThreshold;
// assign positions
for (int i = 0; i < players.Count; i++)
{
if (i >= PlayerPositions.Length)
{
break;
}
players[i].Position =
players[i].OriginalPosition =
PlayerPositions[i];
}
for (int i = 0; i < monsters.Count; i++)
{
if (i >= MonsterPositions.Length)
{
break;
}
monsters[i].Position =
monsters[i].OriginalPosition =
MonsterPositions[i];
}
// sort the monsters by the y coordinates, descending
monsters.Sort(delegate(CombatantMonster monster1, CombatantMonster monster2)
{
return monster2.OriginalPosition.Y.CompareTo(
monster1.OriginalPosition.Y);
});
// create the selection sprites
CreateSelectionSprites();
// create the combat effect sprites
CreateCombatEffectSprites();
// start the first combat turn after a delay
delayType = DelayType.StartCombat;
// start the combat music
AudioManager.PushMusic(TileEngine.Map.CombatMusicCueName);
}
#endregion
#region Fleeing Combat
public static void AttemptFlee()
{
CheckSingleton();
if (!IsPlayersTurn)
{
throw new InvalidOperationException("Only the players may flee.");
}
singleton.delayType = DelayType.FleeAttempt;
Session.Hud.ActionText = "Attempting to Escape...";
}
///
/// The odds of being able to flee this combat, from 0 to 100.
///
private int fleeThreshold = 0;
///
/// Calculate an attempted escape from the combat.
///
/// If true, the escape succeeds.
private bool CalculateFleeAttempt()
{
return (Session.Random.Next(100) < fleeThreshold);
}
#endregion
#region Ending Combat
///
/// End the combat
///
///
private void EndCombat(CombatEndingState combatEndingState)
{
// go back to the non-combat music
AudioManager.PopMusic();
switch (combatEndingState)
{
case CombatEndingState.Victory:
int experienceReward = 0;
int goldReward = 0;
List gearRewards = new List();
List gearRewardNames = new List();
// calculate the rewards from the monsters
foreach (CombatantMonster combatantMonster in monsters)
{
Monster monster = combatantMonster.Monster;
Session.Party.AddMonsterKill(monster);
experienceReward +=
monster.CalculateExperienceReward(Session.Random);
goldReward += monster.CalculateGoldReward(Session.Random);
gearRewardNames.AddRange(
monster.CalculateGearDrop(Session.Random));
}
foreach (string gearRewardName in gearRewardNames)
{
gearRewards.Add(Session.ScreenManager.Game.Content.Load(
Path.Combine(@"Gear", gearRewardName)));
}
// add the reward screen
Session.ScreenManager.AddScreen(new RewardsScreen(
RewardsScreen.RewardScreenMode.Combat, experienceReward,
goldReward, gearRewards));
// remove the fixed combat entry, if this wasn't a random fight
if (FixedCombatEntry != null)
{
Session.RemoveFixedCombat(FixedCombatEntry);
}
break;
case CombatEndingState.Loss: // game over
ScreenManager screenManager = Session.ScreenManager;
// end the session
Session.EndSession();
// add the game-over screen
screenManager.AddScreen(new GameOverScreen());
break;
case CombatEndingState.Fled:
break;
}
// clear the singleton
singleton = null;
}
///
/// Ensure that there is no combat happening right now.
///
public static void ClearCombat()
{
// clear the singleton
if (singleton != null)
{
singleton = null;
}
}
#endregion
#region Updating
///
/// Update the combat engine for this frame.
///
public static void Update(GameTime gameTime)
{
// if there is no active combat, then there's nothing to update
// -- this will be called every frame, so there should be no exception for
// calling this method outside of combat
if (singleton == null)
{
return;
}
// update the singleton
singleton.UpdateCombatEngine(gameTime);
}
///
/// Update the combat engine for this frame.
///
private void UpdateCombatEngine(GameTime gameTime)
{
// check for the end of combat
if (ArePlayersDefeated)
{
EndCombat(CombatEndingState.Loss);
return;
}
else if (AreMonstersDefeated)
{
EndCombat(CombatEndingState.Victory);
return;
}
// update the target selections
if ((highlightedCombatant != null) &&
(highlightedCombatant.CombatAction != null))
{
SetTargets(highlightedCombatant.CombatAction.Target,
highlightedCombatant.CombatAction.AdjacentTargets);
}
// update the delay
UpdateDelay(gameTime.ElapsedGameTime.Milliseconds);
// UpdateDelay might cause combat to end due to a successful escape,
// which will set the singleton to null.
if (singleton == null)
{
return;
}
// update the players
foreach (CombatantPlayer player in players)
{
player.Update(gameTime);
}
// update the monsters
foreach (CombatantMonster monster in monsters)
{
monster.Update(gameTime);
}
// check for completion of the highlighted combatant
if ((delayType == DelayType.NoDelay) &&
(highlightedCombatant != null) && highlightedCombatant.IsTurnTaken)
{
delayType = DelayType.EndCharacterTurn;
}
// handle any player input
HandleInput();
}
///
/// Handle player input that affects the combat engine.
///
private void HandleInput()
{
// only accept input during the players' turn
// -- exit game, etc. is handled by GameplayScreen
if (!IsPlayersTurn || IsPlayersTurnComplete ||
(highlightedCombatant == null))
{
return;
}
#if DEBUG
// cheat key
if (InputManager.IsGamePadRightShoulderTriggered() ||
InputManager.IsKeyTriggered(Keys.W))
{
EndCombat(CombatEndingState.Victory);
return;
}
#endif
// handle input while choosing an action
if (highlightedCombatant.CombatAction != null)
{
// skip if its turn is over or the action is already going
if (highlightedCombatant.IsTurnTaken ||
(highlightedCombatant.CombatAction.Stage !=
CombatAction.CombatActionStage.NotStarted))
{
return;
}
// back out of the action
if (InputManager.IsActionTriggered(InputManager.Action.Back))
{
highlightedCombatant.CombatAction = null;
SetTargets(null, 0);
return;
}
// start the action
if (InputManager.IsActionTriggered(InputManager.Action.Ok))
{
highlightedCombatant.CombatAction.Start();
return;
}
// go to the next target
if (InputManager.IsActionTriggered(InputManager.Action.TargetUp))
{
// cycle through monsters or party members
if (highlightedCombatant.CombatAction.IsOffensive)
{
// find the index of the current target
int newIndex = monsters.FindIndex(
delegate(CombatantMonster monster)
{
return (primaryTargetedCombatant == monster);
});
// find the next living target
do
{
newIndex = (newIndex + 1) % monsters.Count;
}
while (monsters[newIndex].IsDeadOrDying);
// set the new target
highlightedCombatant.CombatAction.Target = monsters[newIndex];
}
else
{
// find the index of the current target
int newIndex = players.FindIndex(
delegate(CombatantPlayer player)
{
return (primaryTargetedCombatant == player);
});
// find the next active, living target
do
{
newIndex = (newIndex + 1) % players.Count;
}
while (players[newIndex].IsDeadOrDying);
// set the new target
highlightedCombatant.CombatAction.Target = players[newIndex];
}
return;
}
// go to the previous target
else if (InputManager.IsActionTriggered(InputManager.Action.TargetDown))
{
// cycle through monsters or party members
if (highlightedCombatant.CombatAction.IsOffensive)
{
// find the index of the current target
int newIndex = monsters.FindIndex(
delegate(CombatantMonster monster)
{
return (primaryTargetedCombatant == monster);
});
// find the previous active, living target
do
{
newIndex--;
while (newIndex < 0)
{
newIndex += monsters.Count;
}
}
while (monsters[newIndex].IsDeadOrDying);
// set the new target
highlightedCombatant.CombatAction.Target = monsters[newIndex];
}
else
{
// find the index of the current target
int newIndex = players.FindIndex(
delegate(CombatantPlayer player)
{
return (primaryTargetedCombatant == player);
});
// find the previous living target
do
{
newIndex--;
while (newIndex < 0)
{
newIndex += players.Count;
}
}
while (players[newIndex].IsDeadOrDying);
// set the new target
highlightedCombatant.CombatAction.Target = players[newIndex];
}
return;
}
}
else // choosing which character will act
{
// move to the previous living character
if (InputManager.IsActionTriggered(
InputManager.Action.ActiveCharacterLeft))
{
int newHighlightedPlayer = highlightedPlayer;
do
{
newHighlightedPlayer--;
while (newHighlightedPlayer < 0)
{
newHighlightedPlayer += players.Count;
}
}
while (players[newHighlightedPlayer].IsDeadOrDying ||
players[newHighlightedPlayer].IsTurnTaken);
if (newHighlightedPlayer != highlightedPlayer)
{
highlightedPlayer = newHighlightedPlayer;
BeginPlayerTurn(players[highlightedPlayer]);
}
return;
}
// move to the next living character
else if (InputManager.IsActionTriggered(
InputManager.Action.ActiveCharacterRight))
{
int newHighlightedPlayer = highlightedPlayer;
do
{
newHighlightedPlayer =
(newHighlightedPlayer + 1) % players.Count;
}
while (players[newHighlightedPlayer].IsDeadOrDying ||
players[newHighlightedPlayer].IsTurnTaken);
if (newHighlightedPlayer != highlightedPlayer)
{
highlightedPlayer = newHighlightedPlayer;
BeginPlayerTurn(players[highlightedPlayer]);
}
return;
}
Session.Hud.UpdateActionsMenu();
}
}
#endregion
#region Drawing
///
/// Draw the combat for this frame.
///
public static void Draw(GameTime gameTime)
{
// if there is no active combat, then there's nothing to draw
// -- this will be called every frame, so there should be no exception for
// calling this method outside of combat
if (singleton == null)
{
return;
}
// update the singleton
singleton.DrawCombatEngine(gameTime);
}
///
/// Draw the combat for this frame.
///
private void DrawCombatEngine(GameTime gameTime)
{
// draw the players
foreach (CombatantPlayer player in players)
{
player.Draw(gameTime);
}
// draw the monsters
foreach (CombatantMonster monster in monsters)
{
monster.Draw(gameTime);
}
// draw the selection animations
DrawSelectionSprites(gameTime);
// draw the combat effects
DrawCombatEffects(gameTime);
}
#endregion
}
}