CombatEngine.cs 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952
  1. //-----------------------------------------------------------------------------
  2. // CombatEngine.cs
  3. //
  4. // Microsoft XNA Community Game Platform
  5. // Copyright (C) Microsoft Corporation. All rights reserved.
  6. //-----------------------------------------------------------------------------
  7. using System;
  8. using System.Collections.Generic;
  9. using System.IO;
  10. using Microsoft.Xna.Framework;
  11. using Microsoft.Xna.Framework.Content;
  12. using Microsoft.Xna.Framework.Graphics;
  13. using Microsoft.Xna.Framework.Input;
  14. using RolePlaying.Data;
  15. namespace RolePlaying
  16. {
  17. /// <summary>
  18. /// The runtime execution engine for the combat system.
  19. /// </summary>
  20. class CombatEngine
  21. {
  22. /// <summary>
  23. /// The singleton of the combat engine.
  24. /// </summary>
  25. private static CombatEngine singleton = null;
  26. /// <summary>
  27. /// Check to see if there is a combat going on, and throw an exception if not.
  28. /// </summary>
  29. private static void CheckSingleton()
  30. {
  31. if (singleton == null)
  32. {
  33. throw new InvalidOperationException(
  34. "There is no active combat at this time.");
  35. }
  36. }
  37. /// <summary>
  38. /// If true, the combat engine is active and the user is in combat.
  39. /// </summary>
  40. public static bool IsActive
  41. {
  42. get { return (singleton != null); }
  43. }
  44. /// <summary>
  45. /// If true, it is currently the players' turn.
  46. /// </summary>
  47. private bool isPlayersTurn;
  48. /// <summary>
  49. /// If true, it is currently the players' turn.
  50. /// </summary>
  51. public static bool IsPlayersTurn
  52. {
  53. get
  54. {
  55. CheckSingleton();
  56. return singleton.isPlayersTurn;
  57. }
  58. }
  59. /// <summary>
  60. /// The fixed combat used to generate this fight, if any.
  61. /// </summary>
  62. /// <remarks>
  63. /// Used for rewards. Null means it was a random fight with no special rewards.
  64. /// </remarks>
  65. private MapEntry<FixedCombat> fixedCombatEntry;
  66. /// <summary>
  67. /// The fixed combat used to generate this fight, if any.
  68. /// </summary>
  69. /// <remarks>
  70. /// Used for rewards. Null means it was a random fight with no special rewards.
  71. /// </remarks>
  72. public static MapEntry<FixedCombat> FixedCombatEntry
  73. {
  74. get { return (singleton == null ? null : singleton.fixedCombatEntry); }
  75. }
  76. /// <summary>
  77. /// The players involved in the current combat.
  78. /// </summary>
  79. private List<CombatantPlayer> players = null;
  80. /// <summary>
  81. /// The players involved in the current combat.
  82. /// </summary>
  83. public static List<CombatantPlayer> Players
  84. {
  85. get
  86. {
  87. CheckSingleton();
  88. return singleton.players;
  89. }
  90. }
  91. private int highlightedPlayer;
  92. /// <summary>
  93. /// The positions of the players on screen.
  94. /// </summary>
  95. private static readonly Vector2[] PlayerPositions = new Vector2[5]
  96. {
  97. new Vector2(850f, 345f),
  98. new Vector2(980f, 260f),
  99. new Vector2(940f, 440f),
  100. new Vector2(1100f, 200f),
  101. new Vector2(1100f, 490f)
  102. };
  103. /// <summary>
  104. /// Start the given player's combat turn.
  105. /// </summary>
  106. private void BeginPlayerTurn(CombatantPlayer player)
  107. {
  108. // check the parameter
  109. if (player == null)
  110. {
  111. throw new ArgumentNullException("player");
  112. }
  113. // set the highlight sprite
  114. highlightedCombatant = player;
  115. primaryTargetedCombatant = null;
  116. secondaryTargetedCombatants.Clear();
  117. Session.Hud.ActionText = "Choose an Action";
  118. }
  119. /// <summary>
  120. /// Begin the players' turn in this combat round.
  121. /// </summary>
  122. private void BeginPlayersTurn()
  123. {
  124. // set the player-turn
  125. isPlayersTurn = true;
  126. // reset each player for the next combat turn
  127. foreach (CombatantPlayer player in players)
  128. {
  129. // reset the animation of living players
  130. if (!player.IsDeadOrDying)
  131. {
  132. player.State = Character.CharacterState.Idle;
  133. }
  134. // reset the turn-taken flag
  135. player.IsTurnTaken = false;
  136. // clear the combat action
  137. player.CombatAction = null;
  138. // advance each player
  139. player.AdvanceRound();
  140. }
  141. // set the action text on the HUD
  142. Session.Hud.ActionText = "Your Party's Turn";
  143. // find the first player who is alive
  144. highlightedPlayer = 0;
  145. CombatantPlayer firstPlayer = players[highlightedPlayer];
  146. while (firstPlayer.IsTurnTaken || firstPlayer.IsDeadOrDying)
  147. {
  148. highlightedPlayer = (highlightedPlayer + 1) % players.Count;
  149. firstPlayer = players[highlightedPlayer];
  150. }
  151. // start the first player's turn
  152. BeginPlayerTurn(firstPlayer);
  153. }
  154. /// <summary>
  155. /// Check for whether all players have taken their turn.
  156. /// </summary>
  157. private bool IsPlayersTurnComplete
  158. {
  159. get
  160. {
  161. return players.TrueForAll(delegate(CombatantPlayer player)
  162. {
  163. return (player.IsTurnTaken || player.IsDeadOrDying);
  164. });
  165. }
  166. }
  167. /// <summary>
  168. /// Check for whether the players have been wiped out and defeated.
  169. /// </summary>
  170. private bool ArePlayersDefeated
  171. {
  172. get
  173. {
  174. return players.TrueForAll(delegate(CombatantPlayer player)
  175. {
  176. return (player.State == Character.CharacterState.Dead);
  177. });
  178. }
  179. }
  180. /// <summary>
  181. /// Retrieves the first living player, if any.
  182. /// </summary>
  183. private CombatantPlayer FirstPlayerTarget
  184. {
  185. get
  186. {
  187. // if there are no living players, then this is moot
  188. if (ArePlayersDefeated)
  189. {
  190. return null;
  191. }
  192. int playerIndex = 0;
  193. while ((playerIndex < players.Count) &&
  194. players[playerIndex].IsDeadOrDying)
  195. {
  196. playerIndex++;
  197. }
  198. return players[playerIndex];
  199. }
  200. }
  201. /// <summary>
  202. /// The monsters involved in the current combat.
  203. /// </summary>
  204. private List<CombatantMonster> monsters = null;
  205. /// <summary>
  206. /// The monsters involved in the current combat.
  207. /// </summary>
  208. public static List<CombatantMonster> Monsters
  209. {
  210. get
  211. {
  212. CheckSingleton();
  213. return singleton.monsters;
  214. }
  215. }
  216. /// <summary>
  217. /// The positions of the monsters on the screen.
  218. /// </summary>
  219. private static readonly Vector2[] MonsterPositions = new Vector2[5]
  220. {
  221. new Vector2(480f, 345f),
  222. new Vector2(345f, 260f),
  223. new Vector2(370f, 440f),
  224. new Vector2(225f, 200f),
  225. new Vector2(225f, 490f)
  226. };
  227. /// <summary>
  228. /// Start the given player's combat turn.
  229. /// </summary>
  230. private void BeginMonsterTurn(CombatantMonster monster)
  231. {
  232. // if it's null, find a random living monster who has yet to take their turn
  233. if (monster == null)
  234. {
  235. // don't bother if all monsters have finished
  236. if (IsMonstersTurnComplete)
  237. {
  238. return;
  239. }
  240. // pick random living monsters who haven't taken their turn
  241. do
  242. {
  243. monster = monsters[Session.Random.Next(monsters.Count)];
  244. }
  245. while (monster.IsTurnTaken || monster.IsDeadOrDying);
  246. }
  247. // set the highlight sprite
  248. highlightedCombatant = monster;
  249. primaryTargetedCombatant = null;
  250. secondaryTargetedCombatants.Clear();
  251. // choose the action immediate
  252. monster.CombatAction = monster.ArtificialIntelligence.ChooseAction();
  253. }
  254. /// <summary>
  255. /// Begin the monsters' turn in this combat round.
  256. /// </summary>
  257. private void BeginMonstersTurn()
  258. {
  259. // set the monster-turn
  260. isPlayersTurn = false;
  261. // reset each monster for the next combat turn
  262. foreach (CombatantMonster monster in monsters)
  263. {
  264. // reset the animations back to idle
  265. monster.Character.State = Character.CharacterState.Idle;
  266. // reset the turn-taken flag
  267. monster.IsTurnTaken = false;
  268. // clear the combat action
  269. monster.CombatAction = null;
  270. // advance the combatants
  271. monster.AdvanceRound();
  272. }
  273. // set the action text on the HUD
  274. Session.Hud.ActionText = "Enemy Party's Turn";
  275. // start a Session.Random monster's turn
  276. BeginMonsterTurn(null);
  277. }
  278. /// <summary>
  279. /// Check for whether all monsters have taken their turn.
  280. /// </summary>
  281. private bool IsMonstersTurnComplete
  282. {
  283. get
  284. {
  285. return monsters.TrueForAll(delegate(CombatantMonster monster)
  286. {
  287. return (monster.IsTurnTaken || monster.IsDeadOrDying);
  288. });
  289. }
  290. }
  291. /// <summary>
  292. /// Check for whether the monsters have been wiped out and defeated.
  293. /// </summary>
  294. private bool AreMonstersDefeated
  295. {
  296. get
  297. {
  298. return monsters.TrueForAll(delegate(CombatantMonster monster)
  299. {
  300. return (monster.State == Character.CharacterState.Dead);
  301. });
  302. }
  303. }
  304. /// <summary>
  305. /// Retrieves the first living monster, if any.
  306. /// </summary>
  307. private CombatantMonster FirstMonsterTarget
  308. {
  309. get
  310. {
  311. // if there are no living monsters, then this is moot
  312. if (AreMonstersDefeated)
  313. {
  314. return null;
  315. }
  316. int monsterIndex = 0;
  317. while ((monsterIndex < monsters.Count) &&
  318. monsters[monsterIndex].IsDeadOrDying)
  319. {
  320. monsterIndex++;
  321. }
  322. return monsters[monsterIndex];
  323. }
  324. }
  325. /// <summary>
  326. /// The currently highlighted player, if any.
  327. /// </summary>
  328. private Combatant highlightedCombatant;
  329. /// <summary>
  330. /// The currently highlighted player, if any.
  331. /// </summary>
  332. public static Combatant HighlightedCombatant
  333. {
  334. get
  335. {
  336. CheckSingleton();
  337. return singleton.highlightedCombatant;
  338. }
  339. }
  340. /// <summary>
  341. /// The current primary target, if any.
  342. /// </summary>
  343. private Combatant primaryTargetedCombatant;
  344. /// <summary>
  345. /// The current primary target, if any.
  346. /// </summary>
  347. public static Combatant PrimaryTargetedCombatant
  348. {
  349. get
  350. {
  351. CheckSingleton();
  352. return singleton.primaryTargetedCombatant;
  353. }
  354. }
  355. /// <summary>
  356. /// The current secondary targets, if any.
  357. /// </summary>
  358. private List<Combatant> secondaryTargetedCombatants = new List<Combatant>();
  359. /// <summary>
  360. /// The current secondary targets, if any.
  361. /// </summary>
  362. public static List<Combatant> SecondaryTargetedCombatants
  363. {
  364. get
  365. {
  366. CheckSingleton();
  367. return singleton.secondaryTargetedCombatants;
  368. }
  369. }
  370. /// <summary>
  371. /// Retrieves the first living enemy, if any.
  372. /// </summary>
  373. public static Combatant FirstEnemyTarget
  374. {
  375. get
  376. {
  377. CheckSingleton();
  378. if (IsPlayersTurn)
  379. {
  380. return singleton.FirstMonsterTarget;
  381. }
  382. else
  383. {
  384. return singleton.FirstPlayerTarget;
  385. }
  386. }
  387. }
  388. /// <summary>
  389. /// Retrieves the first living ally, if any.
  390. /// </summary>
  391. public static Combatant FirstAllyTarget
  392. {
  393. get
  394. {
  395. CheckSingleton();
  396. if (IsPlayersTurn)
  397. {
  398. return singleton.FirstPlayerTarget;
  399. }
  400. else
  401. {
  402. return singleton.FirstMonsterTarget;
  403. }
  404. }
  405. }
  406. /// <summary>
  407. /// Set the primary and any secondary targets.
  408. /// </summary>
  409. /// <param name="primaryTarget">The desired primary target.</param>
  410. /// <param name="adjacentTargets">
  411. /// The number of simultaneous, adjacent targets affected by this spell.
  412. /// </param>
  413. private void SetTargets(Combatant primaryTarget, int adjacentTargets)
  414. {
  415. // set the primary target
  416. this.primaryTargetedCombatant = primaryTarget;
  417. // set any secondary targets
  418. this.secondaryTargetedCombatants.Clear();
  419. if ((primaryTarget != null) && (adjacentTargets > 0))
  420. {
  421. // find out which side is targeted
  422. bool isPlayerTarget = primaryTarget is CombatantPlayer;
  423. // find the index
  424. int primaryTargetIndex = 0;
  425. if (isPlayerTarget)
  426. {
  427. primaryTargetIndex = players.FindIndex(
  428. delegate(CombatantPlayer player)
  429. {
  430. return (player == primaryTarget);
  431. });
  432. }
  433. else
  434. {
  435. primaryTargetIndex = monsters.FindIndex(
  436. delegate(CombatantMonster monster)
  437. {
  438. return (monster == primaryTarget);
  439. });
  440. }
  441. // add the surrounding indices
  442. for (int i = 1; i <= adjacentTargets; i++)
  443. {
  444. int leftIndex = primaryTargetIndex - i;
  445. if (leftIndex >= 0)
  446. {
  447. secondaryTargetedCombatants.Add(
  448. isPlayerTarget ? players[leftIndex] as Combatant :
  449. monsters[leftIndex] as Combatant);
  450. }
  451. int rightIndex = primaryTargetIndex + i;
  452. if (rightIndex < (isPlayerTarget ? players.Count : monsters.Count))
  453. {
  454. secondaryTargetedCombatants.Add(
  455. isPlayerTarget ? players[rightIndex] as Combatant :
  456. monsters[rightIndex] as Combatant);
  457. }
  458. }
  459. }
  460. }
  461. /// <summary>
  462. /// A combat effect sprite, typically used for damage or healing numbers.
  463. /// </summary>
  464. private class CombatEffect
  465. {
  466. /// <summary>
  467. /// The starting position of the effect on the screen.
  468. /// </summary>
  469. public Vector2 OriginalPosition;
  470. /// <summary>
  471. /// The current position of the effect on the screen.
  472. /// </summary>
  473. protected Vector2 position;
  474. /// <summary>
  475. /// The current position of the effect on the screen.
  476. /// </summary>
  477. public Vector2 Position
  478. {
  479. get { return position; }
  480. }
  481. /// <summary>
  482. /// The text that appears on top of the effect.
  483. /// </summary>
  484. protected string text = String.Empty;
  485. /// <summary>
  486. /// The text that appears on top of the effect.
  487. /// </summary>
  488. public string Text
  489. {
  490. get { return text; }
  491. set
  492. {
  493. text = value;
  494. // recalculate the origin
  495. if (String.IsNullOrEmpty(text))
  496. {
  497. textOrigin = Vector2.Zero;
  498. }
  499. else
  500. {
  501. Vector2 textSize = Fonts.DamageFont.MeasureString(text);
  502. textOrigin = new Vector2(
  503. (float)Math.Ceiling(textSize.X / 2f),
  504. (float)Math.Ceiling(textSize.Y / 2f));
  505. }
  506. }
  507. }
  508. /// <summary>
  509. /// The drawing origin of the text on the effect.
  510. /// </summary>
  511. private Vector2 textOrigin = Vector2.Zero;
  512. /// <summary>
  513. /// The speed at which the effect rises on the screen.
  514. /// </summary>
  515. const int risePerSecond = 100;
  516. /// <summary>
  517. /// The amount which the effect rises on the screen.
  518. /// </summary>
  519. const int riseMaximum = 80;
  520. /// <summary>
  521. /// The amount which the effect has already risen on the screen.
  522. /// </summary>
  523. public float Rise = 0;
  524. /// <summary>
  525. /// If true, the effect has finished rising.
  526. /// </summary>
  527. private bool isRiseComplete = false;
  528. /// <summary>
  529. /// If true, the effect has finished rising.
  530. /// </summary>
  531. public bool IsRiseComplete
  532. {
  533. get { return isRiseComplete; }
  534. }
  535. /// <summary>
  536. /// Updates the combat effect.
  537. /// </summary>
  538. /// <param name="elapsedSeconds">
  539. /// The number of seconds elapsed since the last update.
  540. /// </param>
  541. public virtual void Update(float elapsedSeconds)
  542. {
  543. if (!isRiseComplete)
  544. {
  545. Rise += ((float)risePerSecond * elapsedSeconds);
  546. if (Rise > riseMaximum)
  547. {
  548. Rise = riseMaximum;
  549. isRiseComplete = true;
  550. }
  551. position = new Vector2(
  552. OriginalPosition.X,
  553. OriginalPosition.Y - Rise);
  554. }
  555. }
  556. /// <summary>
  557. /// Draw the combat effect.
  558. /// </summary>
  559. /// <param name="spriteBatch">The SpriteBatch used to draw.</param>
  560. /// <param name="texture">The texture for the effect.</param>
  561. public virtual void Draw(SpriteBatch spriteBatch, Texture2D texture)
  562. {
  563. // check the parameter
  564. if (spriteBatch == null)
  565. {
  566. return;
  567. }
  568. // draw the texture
  569. if (texture != null)
  570. {
  571. spriteBatch.Draw(texture, position, null, Color.White, 0f,
  572. new Vector2(texture.Width / 2, texture.Height / 2), 1f,
  573. SpriteEffects.None, 0.3f * (float)Rise / 200f);
  574. }
  575. // draw the text
  576. if (!String.IsNullOrEmpty(Text))
  577. {
  578. spriteBatch.DrawString(Fonts.DamageFont, text, position,
  579. Color.White, 0f, new Vector2(textOrigin.X, textOrigin.Y), 1f,
  580. SpriteEffects.None, 0.2f * (float)Rise / 200f);
  581. }
  582. }
  583. }
  584. /// <summary>
  585. /// The sprite texture for all damage combat effects.
  586. /// </summary>
  587. private Texture2D damageCombatEffectTexture;
  588. /// <summary>
  589. /// All current damage combat effects.
  590. /// </summary>
  591. private List<CombatEffect> damageCombatEffects = new List<CombatEffect>();
  592. /// <summary>
  593. /// Adds a new damage combat effect to the scene.
  594. /// </summary>
  595. /// <param name="position">The position that the effect starts at.</param>
  596. /// <param name="damage">The damage statistics.</param>
  597. public static void AddNewDamageEffects(Vector2 position,
  598. StatisticsValue damage)
  599. {
  600. int startingRise = 0;
  601. CheckSingleton();
  602. if (damage.HealthPoints != 0)
  603. {
  604. CombatEffect combatEffect = new CombatEffect();
  605. combatEffect.OriginalPosition = position;
  606. combatEffect.Text = "HP\n" + damage.HealthPoints.ToString();
  607. combatEffect.Rise = startingRise;
  608. startingRise -= 5;
  609. singleton.damageCombatEffects.Add(combatEffect);
  610. }
  611. if (damage.MagicPoints != 0)
  612. {
  613. CombatEffect combatEffect = new CombatEffect();
  614. combatEffect.OriginalPosition = position;
  615. combatEffect.Text = "MP\n" + damage.MagicPoints.ToString();
  616. combatEffect.Rise = startingRise;
  617. startingRise -= 5;
  618. singleton.damageCombatEffects.Add(combatEffect);
  619. }
  620. if (damage.PhysicalOffense != 0)
  621. {
  622. CombatEffect combatEffect = new CombatEffect();
  623. combatEffect.OriginalPosition = position;
  624. combatEffect.Text = "PO\n" + damage.PhysicalOffense.ToString();
  625. combatEffect.Rise = startingRise;
  626. startingRise -= 5;
  627. singleton.damageCombatEffects.Add(combatEffect);
  628. }
  629. if (damage.PhysicalDefense != 0)
  630. {
  631. CombatEffect combatEffect = new CombatEffect();
  632. combatEffect.OriginalPosition = position;
  633. combatEffect.Text = "PD\n" + damage.PhysicalDefense.ToString();
  634. combatEffect.Rise = startingRise;
  635. startingRise -= 5;
  636. singleton.damageCombatEffects.Add(combatEffect);
  637. }
  638. if (damage.MagicalOffense != 0)
  639. {
  640. CombatEffect combatEffect = new CombatEffect();
  641. combatEffect.OriginalPosition = position;
  642. combatEffect.Text = "MO\n" + damage.MagicalOffense.ToString();
  643. combatEffect.Rise = startingRise;
  644. startingRise -= 5;
  645. singleton.damageCombatEffects.Add(combatEffect);
  646. }
  647. if (damage.MagicalDefense != 0)
  648. {
  649. CombatEffect combatEffect = new CombatEffect();
  650. combatEffect.OriginalPosition = position;
  651. combatEffect.Text = "MD\n" + damage.MagicalDefense.ToString();
  652. combatEffect.Rise = startingRise;
  653. startingRise -= 5;
  654. singleton.damageCombatEffects.Add(combatEffect);
  655. }
  656. }
  657. /// <summary>
  658. /// The sprite texture for all healing combat effects.
  659. /// </summary>
  660. private Texture2D healingCombatEffectTexture;
  661. /// <summary>
  662. /// All current healing combat effects.
  663. /// </summary>
  664. private List<CombatEffect> healingCombatEffects = new List<CombatEffect>();
  665. /// <summary>
  666. /// Adds a new healing combat effect to the scene.
  667. /// </summary>
  668. /// <param name="position">The position that the effect starts at.</param>
  669. /// <param name="damage">The healing statistics.</param>
  670. public static void AddNewHealingEffects(Vector2 position,
  671. StatisticsValue healing)
  672. {
  673. int startingRise = 0;
  674. CheckSingleton();
  675. if (healing.HealthPoints != 0)
  676. {
  677. CombatEffect combatEffect = new CombatEffect();
  678. combatEffect.OriginalPosition = position;
  679. combatEffect.Text = "HP\n" + healing.HealthPoints.ToString();
  680. combatEffect.Rise = startingRise;
  681. startingRise -= 5;
  682. singleton.healingCombatEffects.Add(combatEffect);
  683. }
  684. if (healing.MagicPoints != 0)
  685. {
  686. CombatEffect combatEffect = new CombatEffect();
  687. combatEffect.OriginalPosition = position;
  688. combatEffect.Text = "MP\n" + healing.MagicPoints.ToString();
  689. combatEffect.Rise = startingRise;
  690. startingRise -= 5;
  691. singleton.healingCombatEffects.Add(combatEffect);
  692. }
  693. if (healing.PhysicalOffense != 0)
  694. {
  695. CombatEffect combatEffect = new CombatEffect();
  696. combatEffect.OriginalPosition = position;
  697. combatEffect.Text = "PO\n" + healing.PhysicalOffense.ToString();
  698. combatEffect.Rise = startingRise;
  699. startingRise -= 5;
  700. singleton.healingCombatEffects.Add(combatEffect);
  701. }
  702. if (healing.PhysicalDefense != 0)
  703. {
  704. CombatEffect combatEffect = new CombatEffect();
  705. combatEffect.OriginalPosition = position;
  706. combatEffect.Text = "PD\n" + healing.PhysicalDefense.ToString();
  707. combatEffect.Rise = startingRise;
  708. startingRise -= 5;
  709. singleton.healingCombatEffects.Add(combatEffect);
  710. }
  711. if (healing.MagicalOffense != 0)
  712. {
  713. CombatEffect combatEffect = new CombatEffect();
  714. combatEffect.OriginalPosition = position;
  715. combatEffect.Text = "MO\n" + healing.MagicalOffense.ToString();
  716. combatEffect.Rise = startingRise;
  717. startingRise -= 5;
  718. singleton.healingCombatEffects.Add(combatEffect);
  719. }
  720. if (healing.MagicalDefense != 0)
  721. {
  722. CombatEffect combatEffect = new CombatEffect();
  723. combatEffect.OriginalPosition = position;
  724. combatEffect.Text = "MD\n" + healing.MagicalDefense.ToString();
  725. combatEffect.Rise = startingRise;
  726. startingRise -= 5;
  727. singleton.healingCombatEffects.Add(combatEffect);
  728. }
  729. }
  730. /// <summary>
  731. /// Load the graphics data for the combat effect sprites.
  732. /// </summary>
  733. private void CreateCombatEffectSprites()
  734. {
  735. ContentManager content = Session.ScreenManager.Game.Content;
  736. damageCombatEffectTexture =
  737. content.Load<Texture2D>(Path.Combine("Textures", "Combat", "DamageIcon"));
  738. healingCombatEffectTexture =
  739. content.Load<Texture2D>(Path.Combine("Textures", "Combat", "HealingIcon"));
  740. }
  741. /// <summary>
  742. /// Draw all combat effect sprites.
  743. /// </summary>
  744. private void DrawCombatEffects(GameTime gameTime)
  745. {
  746. float elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
  747. SpriteBatch spriteBatch = Session.ScreenManager.SpriteBatch;
  748. // update all effects
  749. foreach (CombatEffect combatEffect in damageCombatEffects)
  750. {
  751. combatEffect.Update(elapsedSeconds);
  752. }
  753. foreach (CombatEffect combatEffect in healingCombatEffects)
  754. {
  755. combatEffect.Update(elapsedSeconds);
  756. }
  757. // draw the damage effects
  758. if (damageCombatEffectTexture != null)
  759. {
  760. foreach (CombatEffect combatEffect in damageCombatEffects)
  761. {
  762. combatEffect.Draw(spriteBatch, damageCombatEffectTexture);
  763. }
  764. }
  765. // draw the healing effects
  766. if (healingCombatEffectTexture != null)
  767. {
  768. foreach (CombatEffect combatEffect in healingCombatEffects)
  769. {
  770. combatEffect.Draw(spriteBatch, healingCombatEffectTexture);
  771. }
  772. }
  773. // remove all complete effects
  774. Predicate<CombatEffect> removeCompleteEffects =
  775. delegate(CombatEffect combatEffect)
  776. {
  777. return combatEffect.IsRiseComplete;
  778. };
  779. damageCombatEffects.RemoveAll(removeCompleteEffects);
  780. healingCombatEffects.RemoveAll(removeCompleteEffects);
  781. }
  782. /// <summary>
  783. /// The animating sprite that draws over the highlighted character.
  784. /// </summary>
  785. private AnimatingSprite highlightForegroundSprite = new AnimatingSprite();
  786. /// <summary>
  787. /// The animating sprite that draws behind the highlighted character.
  788. /// </summary>
  789. private AnimatingSprite highlightBackgroundSprite = new AnimatingSprite();
  790. /// <summary>
  791. /// The animating sprite that draws behind the primary target character.
  792. /// </summary>
  793. private AnimatingSprite primaryTargetSprite = new AnimatingSprite();
  794. /// <summary>
  795. /// The animating sprite that draws behind any secondary target characters.
  796. /// </summary>
  797. private AnimatingSprite secondaryTargetSprite = new AnimatingSprite();
  798. /// <summary>
  799. /// Create the selection sprite objects.
  800. /// </summary>
  801. private void CreateSelectionSprites()
  802. {
  803. ContentManager content = Session.ScreenManager.Game.Content;
  804. Point frameDimensions = new Point(76, 58);
  805. highlightForegroundSprite.FramesPerRow = 6;
  806. highlightForegroundSprite.FrameDimensions = frameDimensions;
  807. highlightForegroundSprite.AddAnimation(
  808. new Animation("Selection", 1, 4, 100, true));
  809. highlightForegroundSprite.PlayAnimation(0);
  810. highlightForegroundSprite.SourceOffset =
  811. new Vector2(frameDimensions.X / 2f, 40f);
  812. highlightForegroundSprite.Texture =
  813. content.Load<Texture2D>(Path.Combine("Textures", "Combat", "TilesheetSprangles"));
  814. frameDimensions = new Point(102, 54);
  815. highlightBackgroundSprite.FramesPerRow = 4;
  816. highlightBackgroundSprite.FrameDimensions = frameDimensions;
  817. highlightBackgroundSprite.AddAnimation(
  818. new Animation("Selection", 1, 4, 100, true));
  819. highlightBackgroundSprite.PlayAnimation(0);
  820. highlightBackgroundSprite.SourceOffset =
  821. new Vector2(frameDimensions.X / 2f, frameDimensions.Y / 2f);
  822. highlightBackgroundSprite.Texture =
  823. content.Load<Texture2D>(Path.Combine("Textures", "Combat", "CharSelectionRing"));
  824. primaryTargetSprite.FramesPerRow = 4;
  825. primaryTargetSprite.FrameDimensions = frameDimensions;
  826. primaryTargetSprite.AddAnimation(
  827. new Animation("Selection", 1, 4, 100, true));
  828. primaryTargetSprite.PlayAnimation(0);
  829. primaryTargetSprite.SourceOffset =
  830. new Vector2(frameDimensions.X / 2f, frameDimensions.Y / 2f);
  831. primaryTargetSprite.Texture =
  832. content.Load<Texture2D>(Path.Combine("Textures", "Combat", "Target1SelectionRing"));
  833. secondaryTargetSprite.FramesPerRow = 4;
  834. secondaryTargetSprite.FrameDimensions = frameDimensions;
  835. secondaryTargetSprite.AddAnimation(
  836. new Animation("Selection", 1, 4, 100, true));
  837. secondaryTargetSprite.PlayAnimation(0);
  838. secondaryTargetSprite.SourceOffset =
  839. new Vector2(frameDimensions.X / 2f, frameDimensions.Y / 2f);
  840. secondaryTargetSprite.Texture =
  841. content.Load<Texture2D>(Path.Combine("Textures", "Combat", "Target2SelectionRing"));
  842. }
  843. /// <summary>
  844. /// Draw the highlight sprites.
  845. /// </summary>
  846. private void DrawSelectionSprites(GameTime gameTime)
  847. {
  848. SpriteBatch spriteBatch = Session.ScreenManager.SpriteBatch;
  849. // draw the highlighted-player sprite, if any
  850. if (highlightedCombatant != null)
  851. {
  852. highlightBackgroundSprite.Draw(spriteBatch,
  853. highlightedCombatant.Position,
  854. 1f - (highlightedCombatant.Position.Y - 1) / Session.BACK_BUFFER_HEIGHT);
  855. highlightForegroundSprite.Draw(spriteBatch,
  856. highlightedCombatant.Position,
  857. 1f - (highlightedCombatant.Position.Y + 1) / Session.BACK_BUFFER_HEIGHT);
  858. }
  859. // update the animations
  860. float elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
  861. highlightForegroundSprite.UpdateAnimation(elapsedSeconds);
  862. highlightBackgroundSprite.UpdateAnimation(elapsedSeconds);
  863. primaryTargetSprite.UpdateAnimation(elapsedSeconds);
  864. secondaryTargetSprite.UpdateAnimation(elapsedSeconds);
  865. // draw the primary target sprite and name, if any
  866. if (primaryTargetedCombatant != null)
  867. {
  868. primaryTargetSprite.Draw(spriteBatch,
  869. primaryTargetedCombatant.Position,
  870. 1f - (primaryTargetedCombatant.Position.Y - 1) / Session.BACK_BUFFER_HEIGHT);
  871. if (primaryTargetedCombatant.Character is Monster)
  872. {
  873. Fonts.DrawCenteredText(spriteBatch, Fonts.DamageFont,
  874. #if DEBUG
  875. primaryTargetedCombatant.Character.Name + "\n" +
  876. primaryTargetedCombatant.Statistics.HealthPoints + "/" +
  877. primaryTargetedCombatant.Character.CharacterStatistics.HealthPoints,
  878. #else
  879. primaryTargetedCombatant.Character.Name,
  880. #endif
  881. primaryTargetedCombatant.Position + new Vector2(0f, 42f),
  882. Color.White);
  883. }
  884. }
  885. // draw the secondary target sprites on live enemies, if any
  886. foreach (Combatant combatant in secondaryTargetedCombatants)
  887. {
  888. if (combatant.IsDeadOrDying)
  889. {
  890. continue;
  891. }
  892. secondaryTargetSprite.Draw(spriteBatch,
  893. combatant.Position,
  894. 1f - (combatant.Position.Y - 1) / Session.BACK_BUFFER_HEIGHT);
  895. if (combatant.Character is Monster)
  896. {
  897. Fonts.DrawCenteredText(spriteBatch, Fonts.DamageFont,
  898. #if DEBUG
  899. combatant.Character.Name + "\n" +
  900. combatant.Statistics.HealthPoints + "/" +
  901. combatant.Character.CharacterStatistics.HealthPoints,
  902. #else
  903. combatant.Character.Name,
  904. #endif
  905. combatant.Position + new Vector2(0f, 42f), Color.White);
  906. }
  907. }
  908. }
  909. /// <summary>
  910. /// Varieties of delays that are interspersed throughout the combat flow.
  911. /// </summary>
  912. private enum DelayType
  913. {
  914. /// <summary>
  915. /// No delay at this time.
  916. /// </summary>
  917. NoDelay,
  918. /// <summary>
  919. /// Delay at the start of combat.
  920. /// </summary>
  921. StartCombat,
  922. /// <summary>
  923. /// Delay when one side turn's ends before the other side begins.
  924. /// </summary>
  925. EndRound,
  926. /// <summary>
  927. /// Delay at the end of a character's turn before the next one begins.
  928. /// </summary>
  929. EndCharacterTurn,
  930. /// <summary>
  931. /// Delay before a flee is attempted.
  932. /// </summary>
  933. FleeAttempt,
  934. /// <summary>
  935. /// Delay when the party has fled from combat before combat ends.
  936. /// </summary>
  937. FleeSuccessful,
  938. }
  939. /// <summary>
  940. /// The current delay, if any (otherwise NoDelay).
  941. /// </summary>
  942. private DelayType delayType = DelayType.NoDelay;
  943. /// <summary>
  944. /// Returns true if the combat engine is delaying for any reason.
  945. /// </summary>
  946. public static bool IsDelaying
  947. {
  948. get
  949. {
  950. return (singleton == null ? false :
  951. singleton.delayType != DelayType.NoDelay);
  952. }
  953. }
  954. /// <summary>
  955. /// The duration for all kinds of delays, in milliseconds.
  956. /// </summary>
  957. private const int totalDelay = 1000;
  958. /// <summary>
  959. /// The duration of the delay so far.
  960. /// </summary>
  961. private int currentDelay = 0;
  962. /// <summary>
  963. /// Update any delays in the combat system.
  964. /// </summary>
  965. /// <remarks>
  966. /// This function may cause combat to end, setting the singleton to null.
  967. /// </remarks>
  968. private void UpdateDelay(int elapsedMilliseconds)
  969. {
  970. if (delayType == DelayType.NoDelay)
  971. {
  972. return;
  973. }
  974. // increment the delay
  975. currentDelay += elapsedMilliseconds;
  976. // if the delay is ongoing, then we're done
  977. if (currentDelay < totalDelay)
  978. {
  979. return;
  980. }
  981. currentDelay = 0;
  982. // the delay has ended, so the operation implied by the DelayType happens
  983. switch (delayType)
  984. {
  985. case DelayType.StartCombat:
  986. // determine who goes first and start combat
  987. int whoseTurn = Session.Random.Next(2);
  988. if (whoseTurn == 0)
  989. {
  990. BeginPlayersTurn();
  991. }
  992. else
  993. {
  994. BeginMonstersTurn();
  995. }
  996. delayType = DelayType.NoDelay;
  997. break;
  998. case DelayType.EndCharacterTurn:
  999. if (IsPlayersTurn)
  1000. {
  1001. // check to see if the players' turn is complete
  1002. if (IsPlayersTurnComplete)
  1003. {
  1004. delayType = DelayType.EndRound;
  1005. break;
  1006. }
  1007. // find the next player
  1008. int highlightedIndex = players.FindIndex(
  1009. delegate(CombatantPlayer player)
  1010. {
  1011. return (player ==
  1012. highlightedCombatant as CombatantPlayer);
  1013. });
  1014. int nextIndex = (highlightedIndex + 1) % players.Count;
  1015. while (players[nextIndex].IsDeadOrDying ||
  1016. players[nextIndex].IsTurnTaken)
  1017. {
  1018. nextIndex = (nextIndex + 1) % players.Count;
  1019. }
  1020. BeginPlayerTurn(players[nextIndex]);
  1021. }
  1022. else
  1023. {
  1024. // check to see if the monsters' turn is complete
  1025. if (IsMonstersTurnComplete)
  1026. {
  1027. delayType = DelayType.EndRound;
  1028. break;
  1029. }
  1030. // find the next monster
  1031. BeginMonsterTurn(null);
  1032. }
  1033. delayType = DelayType.NoDelay;
  1034. break;
  1035. case DelayType.EndRound:
  1036. // check for turn completion
  1037. if (IsPlayersTurn && IsPlayersTurnComplete)
  1038. {
  1039. BeginMonstersTurn();
  1040. }
  1041. else if (!IsPlayersTurn && IsMonstersTurnComplete)
  1042. {
  1043. BeginPlayersTurn();
  1044. }
  1045. delayType = DelayType.NoDelay;
  1046. break;
  1047. case DelayType.FleeAttempt:
  1048. if (fleeThreshold <= 0)
  1049. {
  1050. delayType = DelayType.EndCharacterTurn;
  1051. Session.Hud.ActionText = "This Fight Cannot Be Escaped...";
  1052. if (highlightedCombatant != null)
  1053. {
  1054. highlightedCombatant.IsTurnTaken = true;
  1055. }
  1056. }
  1057. else if (CalculateFleeAttempt())
  1058. {
  1059. delayType = DelayType.FleeSuccessful;
  1060. Session.Hud.ActionText = "Your Party Has Fled!";
  1061. }
  1062. else
  1063. {
  1064. delayType = DelayType.EndCharacterTurn;
  1065. Session.Hud.ActionText = "Your Party Failed to Escape!";
  1066. if (highlightedCombatant != null)
  1067. {
  1068. highlightedCombatant.IsTurnTaken = true;
  1069. }
  1070. }
  1071. break;
  1072. case DelayType.FleeSuccessful:
  1073. EndCombat(CombatEndingState.Fled);
  1074. delayType = DelayType.NoDelay;
  1075. break;
  1076. }
  1077. }
  1078. /// <summary>
  1079. /// Generates a list of CombatantPlayer objects from the party members.
  1080. /// </summary>
  1081. private static List<CombatantPlayer> GenerateCombatantsFromParty()
  1082. {
  1083. List<CombatantPlayer> generatedPlayers = new List<CombatantPlayer>();
  1084. foreach (Player player in Session.Party.Players)
  1085. {
  1086. if (generatedPlayers.Count <= PlayerPositions.Length)
  1087. {
  1088. generatedPlayers.Add(new CombatantPlayer(player));
  1089. }
  1090. }
  1091. return generatedPlayers;
  1092. }
  1093. /// <summary>
  1094. /// Start a new combat from the given FixedCombat object.
  1095. /// </summary>
  1096. public static void StartNewCombat(MapEntry<FixedCombat> fixedCombatEntry)
  1097. {
  1098. // check the parameter
  1099. if (fixedCombatEntry == null)
  1100. {
  1101. throw new ArgumentNullException("fixedCombatEntry");
  1102. }
  1103. FixedCombat fixedCombat = fixedCombatEntry.Content;
  1104. if (fixedCombat == null)
  1105. {
  1106. throw new ArgumentException("fixedCombatEntry has no content.");
  1107. }
  1108. // generate the monster combatant list
  1109. List<CombatantMonster> generatedMonsters = new List<CombatantMonster>();
  1110. foreach (ContentEntry<Monster> entry in fixedCombat.Entries)
  1111. {
  1112. for (int i = 0; i < entry.Count; i++)
  1113. {
  1114. generatedMonsters.Add(
  1115. new CombatantMonster(entry.Content));
  1116. }
  1117. }
  1118. // randomize the list of monsters
  1119. List<CombatantMonster> randomizedMonsters = new List<CombatantMonster>();
  1120. while ((generatedMonsters.Count > 0) &&
  1121. (randomizedMonsters.Count <= MonsterPositions.Length))
  1122. {
  1123. int index = Session.Random.Next(generatedMonsters.Count);
  1124. randomizedMonsters.Add(generatedMonsters[index]);
  1125. generatedMonsters.RemoveAt(index);
  1126. }
  1127. // start the combat
  1128. StartNewCombat(GenerateCombatantsFromParty(), randomizedMonsters, 0);
  1129. singleton.fixedCombatEntry = fixedCombatEntry;
  1130. }
  1131. /// <summary>
  1132. /// Start a new combat from the given RandomCombat object.
  1133. /// </summary>
  1134. public static void StartNewCombat(RandomCombat randomCombat)
  1135. {
  1136. // check the parameter
  1137. if (randomCombat == null)
  1138. {
  1139. throw new ArgumentNullException("randomCombat");
  1140. }
  1141. // determine how many monsters will be in the combat
  1142. int monsterCount =
  1143. randomCombat.MonsterCountRange.GenerateValue(Session.Random);
  1144. // determine the total probability
  1145. int totalWeight = 0;
  1146. foreach (WeightedContentEntry<Monster> entry in randomCombat.Entries)
  1147. {
  1148. totalWeight += entry.Weight;
  1149. }
  1150. // generate each monster
  1151. List<CombatantMonster> generatedMonsters = new List<CombatantMonster>();
  1152. for (int i = 0; i < monsterCount; i++)
  1153. {
  1154. int monsterChoice = Session.Random.Next(totalWeight);
  1155. foreach (WeightedContentEntry<Monster> entry in randomCombat.Entries)
  1156. {
  1157. if (monsterChoice < entry.Weight)
  1158. {
  1159. generatedMonsters.Add(
  1160. new CombatantMonster(entry.Content));
  1161. break;
  1162. }
  1163. else
  1164. {
  1165. monsterChoice -= entry.Weight;
  1166. }
  1167. }
  1168. }
  1169. // randomize the list of monsters
  1170. List<CombatantMonster> randomizedMonsters = new List<CombatantMonster>();
  1171. while ((generatedMonsters.Count > 0) &&
  1172. (randomizedMonsters.Count <= MonsterPositions.Length))
  1173. {
  1174. int index = Session.Random.Next(generatedMonsters.Count);
  1175. randomizedMonsters.Add(generatedMonsters[index]);
  1176. generatedMonsters.RemoveAt(index);
  1177. }
  1178. // start the combat
  1179. StartNewCombat(GenerateCombatantsFromParty(), randomizedMonsters,
  1180. randomCombat.FleeProbability);
  1181. }
  1182. /// <summary>
  1183. /// Start a new combat between the party and a group of monsters.
  1184. /// </summary>
  1185. /// <param name="players">The player combatants.</param>
  1186. /// <param name="monsters">The monster combatants.</param>
  1187. /// <param name="fleeThreshold">The odds of success when fleeing.</param>
  1188. private static void StartNewCombat(List<CombatantPlayer> players,
  1189. List<CombatantMonster> monsters, int fleeThreshold)
  1190. {
  1191. // check if we are already in combat
  1192. if (singleton != null)
  1193. {
  1194. throw new InvalidOperationException(
  1195. "There can only be one combat at a time.");
  1196. }
  1197. // create the new CombatEngine object
  1198. singleton = new CombatEngine(players, monsters, fleeThreshold);
  1199. }
  1200. /// <summary>
  1201. /// Construct a new CombatEngine object.
  1202. /// </summary>
  1203. /// <param name="players">The player combatants.</param>
  1204. /// <param name="monsters">The monster combatants.</param>
  1205. /// <param name="fleeThreshold">The odds of success when fleeing.</param>
  1206. private CombatEngine(List<CombatantPlayer> players,
  1207. List<CombatantMonster> monsters, int fleeThreshold)
  1208. {
  1209. // check the parameters
  1210. if ((players == null) || (players.Count <= 0) ||
  1211. (players.Count > PlayerPositions.Length))
  1212. {
  1213. throw new ArgumentException("players");
  1214. }
  1215. if ((monsters == null) || (monsters.Count <= 0) ||
  1216. (monsters.Count > MonsterPositions.Length))
  1217. {
  1218. throw new ArgumentException("monsters");
  1219. }
  1220. // assign the parameters
  1221. this.players = players;
  1222. this.monsters = monsters;
  1223. this.fleeThreshold = fleeThreshold;
  1224. // assign positions
  1225. for (int i = 0; i < players.Count; i++)
  1226. {
  1227. if (i >= PlayerPositions.Length)
  1228. {
  1229. break;
  1230. }
  1231. players[i].Position =
  1232. players[i].OriginalPosition =
  1233. PlayerPositions[i];
  1234. }
  1235. for (int i = 0; i < monsters.Count; i++)
  1236. {
  1237. if (i >= MonsterPositions.Length)
  1238. {
  1239. break;
  1240. }
  1241. monsters[i].Position =
  1242. monsters[i].OriginalPosition =
  1243. MonsterPositions[i];
  1244. }
  1245. // sort the monsters by the y coordinates, descending
  1246. monsters.Sort(delegate(CombatantMonster monster1, CombatantMonster monster2)
  1247. {
  1248. return monster2.OriginalPosition.Y.CompareTo(
  1249. monster1.OriginalPosition.Y);
  1250. });
  1251. // create the selection sprites
  1252. CreateSelectionSprites();
  1253. // create the combat effect sprites
  1254. CreateCombatEffectSprites();
  1255. // start the first combat turn after a delay
  1256. delayType = DelayType.StartCombat;
  1257. // start the combat music
  1258. AudioManager.PushMusic(TileEngine.Map.CombatMusicCueName);
  1259. }
  1260. public static void AttemptFlee()
  1261. {
  1262. CheckSingleton();
  1263. if (!IsPlayersTurn)
  1264. {
  1265. throw new InvalidOperationException("Only the players may flee.");
  1266. }
  1267. singleton.delayType = DelayType.FleeAttempt;
  1268. Session.Hud.ActionText = "Attempting to Escape...";
  1269. }
  1270. /// <summary>
  1271. /// The odds of being able to flee this combat, from 0 to 100.
  1272. /// </summary>
  1273. private int fleeThreshold = 0;
  1274. /// <summary>
  1275. /// Calculate an attempted escape from the combat.
  1276. /// </summary>
  1277. /// <returns>If true, the escape succeeds.</returns>
  1278. private bool CalculateFleeAttempt()
  1279. {
  1280. return (Session.Random.Next(100) < fleeThreshold);
  1281. }
  1282. /// <summary>
  1283. /// End the combat
  1284. /// </summary>
  1285. /// <param name="combatEndState"></param>
  1286. private void EndCombat(CombatEndingState combatEndingState)
  1287. {
  1288. // go back to the non-combat music
  1289. AudioManager.PopMusic();
  1290. switch (combatEndingState)
  1291. {
  1292. case CombatEndingState.Victory:
  1293. int experienceReward = 0;
  1294. int goldReward = 0;
  1295. List<Gear> gearRewards = new List<Gear>();
  1296. List<string> gearRewardNames = new List<string>();
  1297. // calculate the rewards from the monsters
  1298. foreach (CombatantMonster combatantMonster in monsters)
  1299. {
  1300. Monster monster = combatantMonster.Monster;
  1301. Session.Party.AddMonsterKill(monster);
  1302. experienceReward +=
  1303. monster.CalculateExperienceReward(Session.Random);
  1304. goldReward += monster.CalculateGoldReward(Session.Random);
  1305. gearRewardNames.AddRange(
  1306. monster.CalculateGearDrop(Session.Random));
  1307. }
  1308. foreach (string gearRewardName in gearRewardNames)
  1309. {
  1310. gearRewards.Add(Session.ScreenManager.Game.Content.Load<Gear>(
  1311. Path.Combine("Gear", gearRewardName)));
  1312. }
  1313. // add the reward screen
  1314. Session.ScreenManager.AddScreen(new RewardsScreen(
  1315. RewardsScreen.RewardScreenMode.Combat, experienceReward,
  1316. goldReward, gearRewards));
  1317. // remove the fixed combat entry, if this wasn't a random fight
  1318. if (FixedCombatEntry != null)
  1319. {
  1320. Session.RemoveFixedCombat(FixedCombatEntry);
  1321. }
  1322. break;
  1323. case CombatEndingState.Loss: // game over
  1324. ScreenManager screenManager = Session.ScreenManager;
  1325. // end the session
  1326. Session.EndSession();
  1327. // add the game-over screen
  1328. screenManager.AddScreen(new GameOverScreen());
  1329. break;
  1330. case CombatEndingState.Fled:
  1331. break;
  1332. }
  1333. // clear the singleton
  1334. singleton = null;
  1335. }
  1336. /// <summary>
  1337. /// Ensure that there is no combat happening right now.
  1338. /// </summary>
  1339. public static void ClearCombat()
  1340. {
  1341. // clear the singleton
  1342. if (singleton != null)
  1343. {
  1344. singleton = null;
  1345. }
  1346. }
  1347. /// <summary>
  1348. /// Update the combat engine for this frame.
  1349. /// </summary>
  1350. public static void Update(GameTime gameTime)
  1351. {
  1352. // if there is no active combat, then there's nothing to update
  1353. // -- this will be called every frame, so there should be no exception for
  1354. // calling this method outside of combat
  1355. if (singleton == null)
  1356. {
  1357. return;
  1358. }
  1359. // update the singleton
  1360. singleton.UpdateCombatEngine(gameTime);
  1361. }
  1362. /// <summary>
  1363. /// Update the combat engine for this frame.
  1364. /// </summary>
  1365. private void UpdateCombatEngine(GameTime gameTime)
  1366. {
  1367. // check for the end of combat
  1368. if (ArePlayersDefeated)
  1369. {
  1370. EndCombat(CombatEndingState.Loss);
  1371. return;
  1372. }
  1373. else if (AreMonstersDefeated)
  1374. {
  1375. EndCombat(CombatEndingState.Victory);
  1376. return;
  1377. }
  1378. // update the target selections
  1379. if ((highlightedCombatant != null) &&
  1380. (highlightedCombatant.CombatAction != null))
  1381. {
  1382. SetTargets(highlightedCombatant.CombatAction.Target,
  1383. highlightedCombatant.CombatAction.AdjacentTargets);
  1384. }
  1385. // update the delay
  1386. UpdateDelay(gameTime.ElapsedGameTime.Milliseconds);
  1387. // UpdateDelay might cause combat to end due to a successful escape,
  1388. // which will set the singleton to null.
  1389. if (singleton == null)
  1390. {
  1391. return;
  1392. }
  1393. // update the players
  1394. foreach (CombatantPlayer player in players)
  1395. {
  1396. player.Update(gameTime);
  1397. }
  1398. // update the monsters
  1399. foreach (CombatantMonster monster in monsters)
  1400. {
  1401. monster.Update(gameTime);
  1402. }
  1403. // check for completion of the highlighted combatant
  1404. if ((delayType == DelayType.NoDelay) &&
  1405. (highlightedCombatant != null) && highlightedCombatant.IsTurnTaken)
  1406. {
  1407. delayType = DelayType.EndCharacterTurn;
  1408. }
  1409. // handle any player input
  1410. HandleInput();
  1411. }
  1412. /// <summary>
  1413. /// Handle player input that affects the combat engine.
  1414. /// </summary>
  1415. private void HandleInput()
  1416. {
  1417. // only accept input during the players' turn
  1418. // -- exit game, etc. is handled by GameplayScreen
  1419. if (!IsPlayersTurn || IsPlayersTurnComplete ||
  1420. (highlightedCombatant == null))
  1421. {
  1422. return;
  1423. }
  1424. #if DEBUG
  1425. // cheat key
  1426. if (InputManager.IsGamePadRightShoulderTriggered() ||
  1427. InputManager.IsKeyTriggered(Keys.W))
  1428. {
  1429. EndCombat(CombatEndingState.Victory);
  1430. return;
  1431. }
  1432. #endif
  1433. // handle input while choosing an action
  1434. if (highlightedCombatant.CombatAction != null)
  1435. {
  1436. // skip if its turn is over or the action is already going
  1437. if (highlightedCombatant.IsTurnTaken ||
  1438. (highlightedCombatant.CombatAction.Stage !=
  1439. CombatAction.CombatActionStage.NotStarted))
  1440. {
  1441. return;
  1442. }
  1443. // back out of the action
  1444. if (InputManager.IsActionTriggered(InputManager.InputAction.Back))
  1445. {
  1446. highlightedCombatant.CombatAction = null;
  1447. SetTargets(null, 0);
  1448. return;
  1449. }
  1450. // start the action
  1451. if (InputManager.IsActionTriggered(InputManager.InputAction.Ok))
  1452. {
  1453. highlightedCombatant.CombatAction.Start();
  1454. return;
  1455. }
  1456. // go to the next target
  1457. if (InputManager.IsActionTriggered(InputManager.InputAction.TargetUp))
  1458. {
  1459. // cycle through monsters or party members
  1460. if (highlightedCombatant.CombatAction.IsOffensive)
  1461. {
  1462. // find the index of the current target
  1463. int newIndex = monsters.FindIndex(
  1464. delegate(CombatantMonster monster)
  1465. {
  1466. return (primaryTargetedCombatant == monster);
  1467. });
  1468. // find the next living target
  1469. do
  1470. {
  1471. newIndex = (newIndex + 1) % monsters.Count;
  1472. }
  1473. while (monsters[newIndex].IsDeadOrDying);
  1474. // set the new target
  1475. highlightedCombatant.CombatAction.Target = monsters[newIndex];
  1476. }
  1477. else
  1478. {
  1479. // find the index of the current target
  1480. int newIndex = players.FindIndex(
  1481. delegate(CombatantPlayer player)
  1482. {
  1483. return (primaryTargetedCombatant == player);
  1484. });
  1485. // find the next active, living target
  1486. do
  1487. {
  1488. newIndex = (newIndex + 1) % players.Count;
  1489. }
  1490. while (players[newIndex].IsDeadOrDying);
  1491. // set the new target
  1492. highlightedCombatant.CombatAction.Target = players[newIndex];
  1493. }
  1494. return;
  1495. }
  1496. // go to the previous target
  1497. else if (InputManager.IsActionTriggered(InputManager.InputAction.TargetDown))
  1498. {
  1499. // cycle through monsters or party members
  1500. if (highlightedCombatant.CombatAction.IsOffensive)
  1501. {
  1502. // find the index of the current target
  1503. int newIndex = monsters.FindIndex(
  1504. delegate(CombatantMonster monster)
  1505. {
  1506. return (primaryTargetedCombatant == monster);
  1507. });
  1508. // find the previous active, living target
  1509. do
  1510. {
  1511. newIndex--;
  1512. while (newIndex < 0)
  1513. {
  1514. newIndex += monsters.Count;
  1515. }
  1516. }
  1517. while (monsters[newIndex].IsDeadOrDying);
  1518. // set the new target
  1519. highlightedCombatant.CombatAction.Target = monsters[newIndex];
  1520. }
  1521. else
  1522. {
  1523. // find the index of the current target
  1524. int newIndex = players.FindIndex(
  1525. delegate(CombatantPlayer player)
  1526. {
  1527. return (primaryTargetedCombatant == player);
  1528. });
  1529. // find the previous living target
  1530. do
  1531. {
  1532. newIndex--;
  1533. while (newIndex < 0)
  1534. {
  1535. newIndex += players.Count;
  1536. }
  1537. }
  1538. while (players[newIndex].IsDeadOrDying);
  1539. // set the new target
  1540. highlightedCombatant.CombatAction.Target = players[newIndex];
  1541. }
  1542. return;
  1543. }
  1544. }
  1545. else // choosing which character will act
  1546. {
  1547. // move to the previous living character
  1548. if (InputManager.IsActionTriggered(
  1549. InputManager.InputAction.ActiveCharacterLeft))
  1550. {
  1551. int newHighlightedPlayer = highlightedPlayer;
  1552. do
  1553. {
  1554. newHighlightedPlayer--;
  1555. while (newHighlightedPlayer < 0)
  1556. {
  1557. newHighlightedPlayer += players.Count;
  1558. }
  1559. }
  1560. while (players[newHighlightedPlayer].IsDeadOrDying ||
  1561. players[newHighlightedPlayer].IsTurnTaken);
  1562. if (newHighlightedPlayer != highlightedPlayer)
  1563. {
  1564. highlightedPlayer = newHighlightedPlayer;
  1565. BeginPlayerTurn(players[highlightedPlayer]);
  1566. }
  1567. return;
  1568. }
  1569. // move to the next living character
  1570. else if (InputManager.IsActionTriggered(
  1571. InputManager.InputAction.ActiveCharacterRight))
  1572. {
  1573. int newHighlightedPlayer = highlightedPlayer;
  1574. do
  1575. {
  1576. newHighlightedPlayer =
  1577. (newHighlightedPlayer + 1) % players.Count;
  1578. }
  1579. while (players[newHighlightedPlayer].IsDeadOrDying ||
  1580. players[newHighlightedPlayer].IsTurnTaken);
  1581. if (newHighlightedPlayer != highlightedPlayer)
  1582. {
  1583. highlightedPlayer = newHighlightedPlayer;
  1584. BeginPlayerTurn(players[highlightedPlayer]);
  1585. }
  1586. return;
  1587. }
  1588. Session.Hud.UpdateActionsMenu();
  1589. }
  1590. }
  1591. /// <summary>
  1592. /// Draw the combat for this frame.
  1593. /// </summary>
  1594. public static void Draw(GameTime gameTime)
  1595. {
  1596. // if there is no active combat, then there's nothing to draw
  1597. // -- this will be called every frame, so there should be no exception for
  1598. // calling this method outside of combat
  1599. if (singleton == null)
  1600. {
  1601. return;
  1602. }
  1603. // update the singleton
  1604. singleton.DrawCombatEngine(gameTime);
  1605. }
  1606. /// <summary>
  1607. /// Draw the combat for this frame.
  1608. /// </summary>
  1609. private void DrawCombatEngine(GameTime gameTime)
  1610. {
  1611. // draw the selection animations
  1612. DrawSelectionSprites(gameTime);
  1613. // draw the players
  1614. foreach (CombatantPlayer player in players)
  1615. {
  1616. player.Draw(gameTime);
  1617. }
  1618. // draw the monsters
  1619. foreach (CombatantMonster monster in monsters)
  1620. {
  1621. monster.Draw(gameTime);
  1622. }
  1623. // draw the combat effects
  1624. DrawCombatEffects(gameTime);
  1625. }
  1626. }
  1627. }