CombatEngine.cs 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953
  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>(@"Textures\Combat\DamageIcon");
  738. healingCombatEffectTexture =
  739. content.Load<Texture2D>(@"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>(@"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>(@"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>(@"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>(@"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. Viewport viewport = Session.ScreenManager.GraphicsDevice.Viewport;
  850. // update the animations
  851. float elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
  852. highlightForegroundSprite.UpdateAnimation(elapsedSeconds);
  853. highlightBackgroundSprite.UpdateAnimation(elapsedSeconds);
  854. primaryTargetSprite.UpdateAnimation(elapsedSeconds);
  855. secondaryTargetSprite.UpdateAnimation(elapsedSeconds);
  856. // draw the highlighted-player sprite, if any
  857. if (highlightedCombatant != null)
  858. {
  859. highlightBackgroundSprite.Draw(spriteBatch,
  860. highlightedCombatant.Position,
  861. 1f - (highlightedCombatant.Position.Y - 1) / viewport.Height);
  862. highlightForegroundSprite.Draw(spriteBatch,
  863. highlightedCombatant.Position,
  864. 1f - (highlightedCombatant.Position.Y + 1) / viewport.Height);
  865. }
  866. // draw the primary target sprite and name, if any
  867. if (primaryTargetedCombatant != null)
  868. {
  869. primaryTargetSprite.Draw(spriteBatch,
  870. primaryTargetedCombatant.Position,
  871. 1f - (primaryTargetedCombatant.Position.Y - 1) / viewport.Height);
  872. if (primaryTargetedCombatant.Character is Monster)
  873. {
  874. Fonts.DrawCenteredText(spriteBatch, Fonts.DamageFont,
  875. #if DEBUG
  876. primaryTargetedCombatant.Character.Name + "\n" +
  877. primaryTargetedCombatant.Statistics.HealthPoints + "/" +
  878. primaryTargetedCombatant.Character.CharacterStatistics.HealthPoints,
  879. #else
  880. primaryTargetedCombatant.Character.Name,
  881. #endif
  882. primaryTargetedCombatant.Position + new Vector2(0f, 42f),
  883. Color.White);
  884. }
  885. }
  886. // draw the secondary target sprites on live enemies, if any
  887. foreach (Combatant combatant in secondaryTargetedCombatants)
  888. {
  889. if (combatant.IsDeadOrDying)
  890. {
  891. continue;
  892. }
  893. secondaryTargetSprite.Draw(spriteBatch,
  894. combatant.Position,
  895. 1f - (combatant.Position.Y - 1) / viewport.Height);
  896. if (combatant.Character is Monster)
  897. {
  898. Fonts.DrawCenteredText(spriteBatch, Fonts.DamageFont,
  899. #if DEBUG
  900. combatant.Character.Name + "\n" +
  901. combatant.Statistics.HealthPoints + "/" +
  902. combatant.Character.CharacterStatistics.HealthPoints,
  903. #else
  904. combatant.Character.Name,
  905. #endif
  906. combatant.Position + new Vector2(0f, 42f), Color.White);
  907. }
  908. }
  909. }
  910. /// <summary>
  911. /// Varieties of delays that are interspersed throughout the combat flow.
  912. /// </summary>
  913. private enum DelayType
  914. {
  915. /// <summary>
  916. /// No delay at this time.
  917. /// </summary>
  918. NoDelay,
  919. /// <summary>
  920. /// Delay at the start of combat.
  921. /// </summary>
  922. StartCombat,
  923. /// <summary>
  924. /// Delay when one side turn's ends before the other side begins.
  925. /// </summary>
  926. EndRound,
  927. /// <summary>
  928. /// Delay at the end of a character's turn before the next one begins.
  929. /// </summary>
  930. EndCharacterTurn,
  931. /// <summary>
  932. /// Delay before a flee is attempted.
  933. /// </summary>
  934. FleeAttempt,
  935. /// <summary>
  936. /// Delay when the party has fled from combat before combat ends.
  937. /// </summary>
  938. FleeSuccessful,
  939. }
  940. /// <summary>
  941. /// The current delay, if any (otherwise NoDelay).
  942. /// </summary>
  943. private DelayType delayType = DelayType.NoDelay;
  944. /// <summary>
  945. /// Returns true if the combat engine is delaying for any reason.
  946. /// </summary>
  947. public static bool IsDelaying
  948. {
  949. get
  950. {
  951. return (singleton == null ? false :
  952. singleton.delayType != DelayType.NoDelay);
  953. }
  954. }
  955. /// <summary>
  956. /// The duration for all kinds of delays, in milliseconds.
  957. /// </summary>
  958. private const int totalDelay = 1000;
  959. /// <summary>
  960. /// The duration of the delay so far.
  961. /// </summary>
  962. private int currentDelay = 0;
  963. /// <summary>
  964. /// Update any delays in the combat system.
  965. /// </summary>
  966. /// <remarks>
  967. /// This function may cause combat to end, setting the singleton to null.
  968. /// </remarks>
  969. private void UpdateDelay(int elapsedMilliseconds)
  970. {
  971. if (delayType == DelayType.NoDelay)
  972. {
  973. return;
  974. }
  975. // increment the delay
  976. currentDelay += elapsedMilliseconds;
  977. // if the delay is ongoing, then we're done
  978. if (currentDelay < totalDelay)
  979. {
  980. return;
  981. }
  982. currentDelay = 0;
  983. // the delay has ended, so the operation implied by the DelayType happens
  984. switch (delayType)
  985. {
  986. case DelayType.StartCombat:
  987. // determine who goes first and start combat
  988. int whoseTurn = Session.Random.Next(2);
  989. if (whoseTurn == 0)
  990. {
  991. BeginPlayersTurn();
  992. }
  993. else
  994. {
  995. BeginMonstersTurn();
  996. }
  997. delayType = DelayType.NoDelay;
  998. break;
  999. case DelayType.EndCharacterTurn:
  1000. if (IsPlayersTurn)
  1001. {
  1002. // check to see if the players' turn is complete
  1003. if (IsPlayersTurnComplete)
  1004. {
  1005. delayType = DelayType.EndRound;
  1006. break;
  1007. }
  1008. // find the next player
  1009. int highlightedIndex = players.FindIndex(
  1010. delegate(CombatantPlayer player)
  1011. {
  1012. return (player ==
  1013. highlightedCombatant as CombatantPlayer);
  1014. });
  1015. int nextIndex = (highlightedIndex + 1) % players.Count;
  1016. while (players[nextIndex].IsDeadOrDying ||
  1017. players[nextIndex].IsTurnTaken)
  1018. {
  1019. nextIndex = (nextIndex + 1) % players.Count;
  1020. }
  1021. BeginPlayerTurn(players[nextIndex]);
  1022. }
  1023. else
  1024. {
  1025. // check to see if the monsters' turn is complete
  1026. if (IsMonstersTurnComplete)
  1027. {
  1028. delayType = DelayType.EndRound;
  1029. break;
  1030. }
  1031. // find the next monster
  1032. BeginMonsterTurn(null);
  1033. }
  1034. delayType = DelayType.NoDelay;
  1035. break;
  1036. case DelayType.EndRound:
  1037. // check for turn completion
  1038. if (IsPlayersTurn && IsPlayersTurnComplete)
  1039. {
  1040. BeginMonstersTurn();
  1041. }
  1042. else if (!IsPlayersTurn && IsMonstersTurnComplete)
  1043. {
  1044. BeginPlayersTurn();
  1045. }
  1046. delayType = DelayType.NoDelay;
  1047. break;
  1048. case DelayType.FleeAttempt:
  1049. if (fleeThreshold <= 0)
  1050. {
  1051. delayType = DelayType.EndCharacterTurn;
  1052. Session.Hud.ActionText = "This Fight Cannot Be Escaped...";
  1053. if (highlightedCombatant != null)
  1054. {
  1055. highlightedCombatant.IsTurnTaken = true;
  1056. }
  1057. }
  1058. else if (CalculateFleeAttempt())
  1059. {
  1060. delayType = DelayType.FleeSuccessful;
  1061. Session.Hud.ActionText = "Your Party Has Fled!";
  1062. }
  1063. else
  1064. {
  1065. delayType = DelayType.EndCharacterTurn;
  1066. Session.Hud.ActionText = "Your Party Failed to Escape!";
  1067. if (highlightedCombatant != null)
  1068. {
  1069. highlightedCombatant.IsTurnTaken = true;
  1070. }
  1071. }
  1072. break;
  1073. case DelayType.FleeSuccessful:
  1074. EndCombat(CombatEndingState.Fled);
  1075. delayType = DelayType.NoDelay;
  1076. break;
  1077. }
  1078. }
  1079. /// <summary>
  1080. /// Generates a list of CombatantPlayer objects from the party members.
  1081. /// </summary>
  1082. private static List<CombatantPlayer> GenerateCombatantsFromParty()
  1083. {
  1084. List<CombatantPlayer> generatedPlayers = new List<CombatantPlayer>();
  1085. foreach (Player player in Session.Party.Players)
  1086. {
  1087. if (generatedPlayers.Count <= PlayerPositions.Length)
  1088. {
  1089. generatedPlayers.Add(new CombatantPlayer(player));
  1090. }
  1091. }
  1092. return generatedPlayers;
  1093. }
  1094. /// <summary>
  1095. /// Start a new combat from the given FixedCombat object.
  1096. /// </summary>
  1097. public static void StartNewCombat(MapEntry<FixedCombat> fixedCombatEntry)
  1098. {
  1099. // check the parameter
  1100. if (fixedCombatEntry == null)
  1101. {
  1102. throw new ArgumentNullException("fixedCombatEntry");
  1103. }
  1104. FixedCombat fixedCombat = fixedCombatEntry.Content;
  1105. if (fixedCombat == null)
  1106. {
  1107. throw new ArgumentException("fixedCombatEntry has no content.");
  1108. }
  1109. // generate the monster combatant list
  1110. List<CombatantMonster> generatedMonsters = new List<CombatantMonster>();
  1111. foreach (ContentEntry<Monster> entry in fixedCombat.Entries)
  1112. {
  1113. for (int i = 0; i < entry.Count; i++)
  1114. {
  1115. generatedMonsters.Add(
  1116. new CombatantMonster(entry.Content));
  1117. }
  1118. }
  1119. // randomize the list of monsters
  1120. List<CombatantMonster> randomizedMonsters = new List<CombatantMonster>();
  1121. while ((generatedMonsters.Count > 0) &&
  1122. (randomizedMonsters.Count <= MonsterPositions.Length))
  1123. {
  1124. int index = Session.Random.Next(generatedMonsters.Count);
  1125. randomizedMonsters.Add(generatedMonsters[index]);
  1126. generatedMonsters.RemoveAt(index);
  1127. }
  1128. // start the combat
  1129. StartNewCombat(GenerateCombatantsFromParty(), randomizedMonsters, 0);
  1130. singleton.fixedCombatEntry = fixedCombatEntry;
  1131. }
  1132. /// <summary>
  1133. /// Start a new combat from the given RandomCombat object.
  1134. /// </summary>
  1135. public static void StartNewCombat(RandomCombat randomCombat)
  1136. {
  1137. // check the parameter
  1138. if (randomCombat == null)
  1139. {
  1140. throw new ArgumentNullException("randomCombat");
  1141. }
  1142. // determine how many monsters will be in the combat
  1143. int monsterCount =
  1144. randomCombat.MonsterCountRange.GenerateValue(Session.Random);
  1145. // determine the total probability
  1146. int totalWeight = 0;
  1147. foreach (WeightedContentEntry<Monster> entry in randomCombat.Entries)
  1148. {
  1149. totalWeight += entry.Weight;
  1150. }
  1151. // generate each monster
  1152. List<CombatantMonster> generatedMonsters = new List<CombatantMonster>();
  1153. for (int i = 0; i < monsterCount; i++)
  1154. {
  1155. int monsterChoice = Session.Random.Next(totalWeight);
  1156. foreach (WeightedContentEntry<Monster> entry in randomCombat.Entries)
  1157. {
  1158. if (monsterChoice < entry.Weight)
  1159. {
  1160. generatedMonsters.Add(
  1161. new CombatantMonster(entry.Content));
  1162. break;
  1163. }
  1164. else
  1165. {
  1166. monsterChoice -= entry.Weight;
  1167. }
  1168. }
  1169. }
  1170. // randomize the list of monsters
  1171. List<CombatantMonster> randomizedMonsters = new List<CombatantMonster>();
  1172. while ((generatedMonsters.Count > 0) &&
  1173. (randomizedMonsters.Count <= MonsterPositions.Length))
  1174. {
  1175. int index = Session.Random.Next(generatedMonsters.Count);
  1176. randomizedMonsters.Add(generatedMonsters[index]);
  1177. generatedMonsters.RemoveAt(index);
  1178. }
  1179. // start the combat
  1180. StartNewCombat(GenerateCombatantsFromParty(), randomizedMonsters,
  1181. randomCombat.FleeProbability);
  1182. }
  1183. /// <summary>
  1184. /// Start a new combat between the party and a group of monsters.
  1185. /// </summary>
  1186. /// <param name="players">The player combatants.</param>
  1187. /// <param name="monsters">The monster combatants.</param>
  1188. /// <param name="fleeThreshold">The odds of success when fleeing.</param>
  1189. private static void StartNewCombat(List<CombatantPlayer> players,
  1190. List<CombatantMonster> monsters, int fleeThreshold)
  1191. {
  1192. // check if we are already in combat
  1193. if (singleton != null)
  1194. {
  1195. throw new InvalidOperationException(
  1196. "There can only be one combat at a time.");
  1197. }
  1198. // create the new CombatEngine object
  1199. singleton = new CombatEngine(players, monsters, fleeThreshold);
  1200. }
  1201. /// <summary>
  1202. /// Construct a new CombatEngine object.
  1203. /// </summary>
  1204. /// <param name="players">The player combatants.</param>
  1205. /// <param name="monsters">The monster combatants.</param>
  1206. /// <param name="fleeThreshold">The odds of success when fleeing.</param>
  1207. private CombatEngine(List<CombatantPlayer> players,
  1208. List<CombatantMonster> monsters, int fleeThreshold)
  1209. {
  1210. // check the parameters
  1211. if ((players == null) || (players.Count <= 0) ||
  1212. (players.Count > PlayerPositions.Length))
  1213. {
  1214. throw new ArgumentException("players");
  1215. }
  1216. if ((monsters == null) || (monsters.Count <= 0) ||
  1217. (monsters.Count > MonsterPositions.Length))
  1218. {
  1219. throw new ArgumentException("monsters");
  1220. }
  1221. // assign the parameters
  1222. this.players = players;
  1223. this.monsters = monsters;
  1224. this.fleeThreshold = fleeThreshold;
  1225. // assign positions
  1226. for (int i = 0; i < players.Count; i++)
  1227. {
  1228. if (i >= PlayerPositions.Length)
  1229. {
  1230. break;
  1231. }
  1232. players[i].Position =
  1233. players[i].OriginalPosition =
  1234. PlayerPositions[i];
  1235. }
  1236. for (int i = 0; i < monsters.Count; i++)
  1237. {
  1238. if (i >= MonsterPositions.Length)
  1239. {
  1240. break;
  1241. }
  1242. monsters[i].Position =
  1243. monsters[i].OriginalPosition =
  1244. MonsterPositions[i];
  1245. }
  1246. // sort the monsters by the y coordinates, descending
  1247. monsters.Sort(delegate(CombatantMonster monster1, CombatantMonster monster2)
  1248. {
  1249. return monster2.OriginalPosition.Y.CompareTo(
  1250. monster1.OriginalPosition.Y);
  1251. });
  1252. // create the selection sprites
  1253. CreateSelectionSprites();
  1254. // create the combat effect sprites
  1255. CreateCombatEffectSprites();
  1256. // start the first combat turn after a delay
  1257. delayType = DelayType.StartCombat;
  1258. // start the combat music
  1259. AudioManager.PushMusic(TileEngine.Map.CombatMusicCueName);
  1260. }
  1261. public static void AttemptFlee()
  1262. {
  1263. CheckSingleton();
  1264. if (!IsPlayersTurn)
  1265. {
  1266. throw new InvalidOperationException("Only the players may flee.");
  1267. }
  1268. singleton.delayType = DelayType.FleeAttempt;
  1269. Session.Hud.ActionText = "Attempting to Escape...";
  1270. }
  1271. /// <summary>
  1272. /// The odds of being able to flee this combat, from 0 to 100.
  1273. /// </summary>
  1274. private int fleeThreshold = 0;
  1275. /// <summary>
  1276. /// Calculate an attempted escape from the combat.
  1277. /// </summary>
  1278. /// <returns>If true, the escape succeeds.</returns>
  1279. private bool CalculateFleeAttempt()
  1280. {
  1281. return (Session.Random.Next(100) < fleeThreshold);
  1282. }
  1283. /// <summary>
  1284. /// End the combat
  1285. /// </summary>
  1286. /// <param name="combatEndState"></param>
  1287. private void EndCombat(CombatEndingState combatEndingState)
  1288. {
  1289. // go back to the non-combat music
  1290. AudioManager.PopMusic();
  1291. switch (combatEndingState)
  1292. {
  1293. case CombatEndingState.Victory:
  1294. int experienceReward = 0;
  1295. int goldReward = 0;
  1296. List<Gear> gearRewards = new List<Gear>();
  1297. List<string> gearRewardNames = new List<string>();
  1298. // calculate the rewards from the monsters
  1299. foreach (CombatantMonster combatantMonster in monsters)
  1300. {
  1301. Monster monster = combatantMonster.Monster;
  1302. Session.Party.AddMonsterKill(monster);
  1303. experienceReward +=
  1304. monster.CalculateExperienceReward(Session.Random);
  1305. goldReward += monster.CalculateGoldReward(Session.Random);
  1306. gearRewardNames.AddRange(
  1307. monster.CalculateGearDrop(Session.Random));
  1308. }
  1309. foreach (string gearRewardName in gearRewardNames)
  1310. {
  1311. gearRewards.Add(Session.ScreenManager.Game.Content.Load<Gear>(
  1312. Path.Combine(@"Gear", gearRewardName)));
  1313. }
  1314. // add the reward screen
  1315. Session.ScreenManager.AddScreen(new RewardsScreen(
  1316. RewardsScreen.RewardScreenMode.Combat, experienceReward,
  1317. goldReward, gearRewards));
  1318. // remove the fixed combat entry, if this wasn't a random fight
  1319. if (FixedCombatEntry != null)
  1320. {
  1321. Session.RemoveFixedCombat(FixedCombatEntry);
  1322. }
  1323. break;
  1324. case CombatEndingState.Loss: // game over
  1325. ScreenManager screenManager = Session.ScreenManager;
  1326. // end the session
  1327. Session.EndSession();
  1328. // add the game-over screen
  1329. screenManager.AddScreen(new GameOverScreen());
  1330. break;
  1331. case CombatEndingState.Fled:
  1332. break;
  1333. }
  1334. // clear the singleton
  1335. singleton = null;
  1336. }
  1337. /// <summary>
  1338. /// Ensure that there is no combat happening right now.
  1339. /// </summary>
  1340. public static void ClearCombat()
  1341. {
  1342. // clear the singleton
  1343. if (singleton != null)
  1344. {
  1345. singleton = null;
  1346. }
  1347. }
  1348. /// <summary>
  1349. /// Update the combat engine for this frame.
  1350. /// </summary>
  1351. public static void Update(GameTime gameTime)
  1352. {
  1353. // if there is no active combat, then there's nothing to update
  1354. // -- this will be called every frame, so there should be no exception for
  1355. // calling this method outside of combat
  1356. if (singleton == null)
  1357. {
  1358. return;
  1359. }
  1360. // update the singleton
  1361. singleton.UpdateCombatEngine(gameTime);
  1362. }
  1363. /// <summary>
  1364. /// Update the combat engine for this frame.
  1365. /// </summary>
  1366. private void UpdateCombatEngine(GameTime gameTime)
  1367. {
  1368. // check for the end of combat
  1369. if (ArePlayersDefeated)
  1370. {
  1371. EndCombat(CombatEndingState.Loss);
  1372. return;
  1373. }
  1374. else if (AreMonstersDefeated)
  1375. {
  1376. EndCombat(CombatEndingState.Victory);
  1377. return;
  1378. }
  1379. // update the target selections
  1380. if ((highlightedCombatant != null) &&
  1381. (highlightedCombatant.CombatAction != null))
  1382. {
  1383. SetTargets(highlightedCombatant.CombatAction.Target,
  1384. highlightedCombatant.CombatAction.AdjacentTargets);
  1385. }
  1386. // update the delay
  1387. UpdateDelay(gameTime.ElapsedGameTime.Milliseconds);
  1388. // UpdateDelay might cause combat to end due to a successful escape,
  1389. // which will set the singleton to null.
  1390. if (singleton == null)
  1391. {
  1392. return;
  1393. }
  1394. // update the players
  1395. foreach (CombatantPlayer player in players)
  1396. {
  1397. player.Update(gameTime);
  1398. }
  1399. // update the monsters
  1400. foreach (CombatantMonster monster in monsters)
  1401. {
  1402. monster.Update(gameTime);
  1403. }
  1404. // check for completion of the highlighted combatant
  1405. if ((delayType == DelayType.NoDelay) &&
  1406. (highlightedCombatant != null) && highlightedCombatant.IsTurnTaken)
  1407. {
  1408. delayType = DelayType.EndCharacterTurn;
  1409. }
  1410. // handle any player input
  1411. HandleInput();
  1412. }
  1413. /// <summary>
  1414. /// Handle player input that affects the combat engine.
  1415. /// </summary>
  1416. private void HandleInput()
  1417. {
  1418. // only accept input during the players' turn
  1419. // -- exit game, etc. is handled by GameplayScreen
  1420. if (!IsPlayersTurn || IsPlayersTurnComplete ||
  1421. (highlightedCombatant == null))
  1422. {
  1423. return;
  1424. }
  1425. #if DEBUG
  1426. // cheat key
  1427. if (InputManager.IsGamePadRightShoulderTriggered() ||
  1428. InputManager.IsKeyTriggered(Keys.W))
  1429. {
  1430. EndCombat(CombatEndingState.Victory);
  1431. return;
  1432. }
  1433. #endif
  1434. // handle input while choosing an action
  1435. if (highlightedCombatant.CombatAction != null)
  1436. {
  1437. // skip if its turn is over or the action is already going
  1438. if (highlightedCombatant.IsTurnTaken ||
  1439. (highlightedCombatant.CombatAction.Stage !=
  1440. CombatAction.CombatActionStage.NotStarted))
  1441. {
  1442. return;
  1443. }
  1444. // back out of the action
  1445. if (InputManager.IsActionTriggered(InputManager.Action.Back))
  1446. {
  1447. highlightedCombatant.CombatAction = null;
  1448. SetTargets(null, 0);
  1449. return;
  1450. }
  1451. // start the action
  1452. if (InputManager.IsActionTriggered(InputManager.Action.Ok))
  1453. {
  1454. highlightedCombatant.CombatAction.Start();
  1455. return;
  1456. }
  1457. // go to the next target
  1458. if (InputManager.IsActionTriggered(InputManager.Action.TargetUp))
  1459. {
  1460. // cycle through monsters or party members
  1461. if (highlightedCombatant.CombatAction.IsOffensive)
  1462. {
  1463. // find the index of the current target
  1464. int newIndex = monsters.FindIndex(
  1465. delegate(CombatantMonster monster)
  1466. {
  1467. return (primaryTargetedCombatant == monster);
  1468. });
  1469. // find the next living target
  1470. do
  1471. {
  1472. newIndex = (newIndex + 1) % monsters.Count;
  1473. }
  1474. while (monsters[newIndex].IsDeadOrDying);
  1475. // set the new target
  1476. highlightedCombatant.CombatAction.Target = monsters[newIndex];
  1477. }
  1478. else
  1479. {
  1480. // find the index of the current target
  1481. int newIndex = players.FindIndex(
  1482. delegate(CombatantPlayer player)
  1483. {
  1484. return (primaryTargetedCombatant == player);
  1485. });
  1486. // find the next active, living target
  1487. do
  1488. {
  1489. newIndex = (newIndex + 1) % players.Count;
  1490. }
  1491. while (players[newIndex].IsDeadOrDying);
  1492. // set the new target
  1493. highlightedCombatant.CombatAction.Target = players[newIndex];
  1494. }
  1495. return;
  1496. }
  1497. // go to the previous target
  1498. else if (InputManager.IsActionTriggered(InputManager.Action.TargetDown))
  1499. {
  1500. // cycle through monsters or party members
  1501. if (highlightedCombatant.CombatAction.IsOffensive)
  1502. {
  1503. // find the index of the current target
  1504. int newIndex = monsters.FindIndex(
  1505. delegate(CombatantMonster monster)
  1506. {
  1507. return (primaryTargetedCombatant == monster);
  1508. });
  1509. // find the previous active, living target
  1510. do
  1511. {
  1512. newIndex--;
  1513. while (newIndex < 0)
  1514. {
  1515. newIndex += monsters.Count;
  1516. }
  1517. }
  1518. while (monsters[newIndex].IsDeadOrDying);
  1519. // set the new target
  1520. highlightedCombatant.CombatAction.Target = monsters[newIndex];
  1521. }
  1522. else
  1523. {
  1524. // find the index of the current target
  1525. int newIndex = players.FindIndex(
  1526. delegate(CombatantPlayer player)
  1527. {
  1528. return (primaryTargetedCombatant == player);
  1529. });
  1530. // find the previous living target
  1531. do
  1532. {
  1533. newIndex--;
  1534. while (newIndex < 0)
  1535. {
  1536. newIndex += players.Count;
  1537. }
  1538. }
  1539. while (players[newIndex].IsDeadOrDying);
  1540. // set the new target
  1541. highlightedCombatant.CombatAction.Target = players[newIndex];
  1542. }
  1543. return;
  1544. }
  1545. }
  1546. else // choosing which character will act
  1547. {
  1548. // move to the previous living character
  1549. if (InputManager.IsActionTriggered(
  1550. InputManager.Action.ActiveCharacterLeft))
  1551. {
  1552. int newHighlightedPlayer = highlightedPlayer;
  1553. do
  1554. {
  1555. newHighlightedPlayer--;
  1556. while (newHighlightedPlayer < 0)
  1557. {
  1558. newHighlightedPlayer += players.Count;
  1559. }
  1560. }
  1561. while (players[newHighlightedPlayer].IsDeadOrDying ||
  1562. players[newHighlightedPlayer].IsTurnTaken);
  1563. if (newHighlightedPlayer != highlightedPlayer)
  1564. {
  1565. highlightedPlayer = newHighlightedPlayer;
  1566. BeginPlayerTurn(players[highlightedPlayer]);
  1567. }
  1568. return;
  1569. }
  1570. // move to the next living character
  1571. else if (InputManager.IsActionTriggered(
  1572. InputManager.Action.ActiveCharacterRight))
  1573. {
  1574. int newHighlightedPlayer = highlightedPlayer;
  1575. do
  1576. {
  1577. newHighlightedPlayer =
  1578. (newHighlightedPlayer + 1) % players.Count;
  1579. }
  1580. while (players[newHighlightedPlayer].IsDeadOrDying ||
  1581. players[newHighlightedPlayer].IsTurnTaken);
  1582. if (newHighlightedPlayer != highlightedPlayer)
  1583. {
  1584. highlightedPlayer = newHighlightedPlayer;
  1585. BeginPlayerTurn(players[highlightedPlayer]);
  1586. }
  1587. return;
  1588. }
  1589. Session.Hud.UpdateActionsMenu();
  1590. }
  1591. }
  1592. /// <summary>
  1593. /// Draw the combat for this frame.
  1594. /// </summary>
  1595. public static void Draw(GameTime gameTime)
  1596. {
  1597. // if there is no active combat, then there's nothing to draw
  1598. // -- this will be called every frame, so there should be no exception for
  1599. // calling this method outside of combat
  1600. if (singleton == null)
  1601. {
  1602. return;
  1603. }
  1604. // update the singleton
  1605. singleton.DrawCombatEngine(gameTime);
  1606. }
  1607. /// <summary>
  1608. /// Draw the combat for this frame.
  1609. /// </summary>
  1610. private void DrawCombatEngine(GameTime gameTime)
  1611. {
  1612. // draw the players
  1613. foreach (CombatantPlayer player in players)
  1614. {
  1615. player.Draw(gameTime);
  1616. }
  1617. // draw the monsters
  1618. foreach (CombatantMonster monster in monsters)
  1619. {
  1620. monster.Draw(gameTime);
  1621. }
  1622. // draw the selection animations
  1623. DrawSelectionSprites(gameTime);
  1624. // draw the combat effects
  1625. DrawCombatEffects(gameTime);
  1626. }
  1627. }
  1628. }