I am trying to set up a Turn-based combat system in Unity where I can step through each of the character's turns, and on the player's turn, get some input. I have heard coroutines & IEnumerators can help me do this, but obviously I am not fully grasping how this works because currently, I can watch the playerCharacter's currentHealth drop all the way down to 0 even before it reaches their first turn.
I was hoping someone could help me understand where I am going wrong. Here is my CombatSystem class:
public enum CombatState { START, ENEMYTURN, PLAYERTURN, WON, LOST }
public class CombatSystem : MonoBehaviour
{
public List<Character> currentEnemiesInCombat;
public CombatState state;
private GameController controller;
private void Awake()
{
controller = GetComponent<GameController>();
}
//Need to create an action response similar to openhiddenroom that will
//activate combat upon coming across revealed enemies, then CombatSystem
//will handle what happens below
public void StartCombat(PlayerCharacter playerCharacter, List<Character> triggeredEnemies)
{
//Bring up enemy illustrations, possibly resize canvas
//Roll initiative for each character
state = CombatState.START;
controller.ResizeDisplayTextForCombat();
controller.LogStringWithReturn("CombatSystem.cs DEBUG: YOU ARE IN COMBAT NOW");
SetUpCharacterInitiatives(playerCharacter, triggeredEnemies);
StartCoroutine(TurnBasedCombatRoutine(playerCharacter, triggeredEnemies));
}
//THIS NEEDS REWORKING FROM HERE DOWN-------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------------------------------
IEnumerator TurnBasedCombatRoutine(PlayerCharacter playerCharacter, List<Character> triggeredEnemies)
{
var allCharactersInBattle = triggeredEnemies.Concat(new[] { playerCharacter }).OrderByDescending(c => c.initiative);
//Keep the fight going while the player is above 0 hp and enemies still remain
while (playerCharacter.currentHealth > 0 && triggeredEnemies.Count > 0)
{
foreach (Character character in allCharactersInBattle)
{
//Check if the character and playercharacter are still alive
//because we don't want enemies taking more turnns if the player is
//already dead
if (character.currentHealth > 0 && playerCharacter.currentHealth > 0)
{
controller.LogStringWithReturn("CombatSystem.cs DEBUG: " + character.charName + "'s turn");
//Perform actions for the current character's turn
//yield if it is the player's turn
if (character != controller.playerCharacter)
{
state = CombatState.ENEMYTURN;
PerformEnemyAction(character);
}
else
{
//NEED TO FIGURE OUT HOW TO MAKE THIS YIELD UNTIL A VALID USER INPUT
state = CombatState.PLAYERTURN;
controller.LogStringWithReturn("It is your turn...");
yield return null;
}
// Check for defeated enemies after each turn
triggeredEnemies.RemoveAll(enemy => isDead(enemy));
// Break out of the loop if all enemies are defeated
if (triggeredEnemies.Count == 0)
{
state = CombatState.WON;
controller.LogStringWithReturn("You have defeated all of your foes. It is safe to move forward.");
break;
}
}
}
}
EndCombat();
}
//Roll initiatives for all characters, including player
//Make sure they don't have the same initiative amounts
void SetUpCharacterInitiatives(PlayerCharacter playerCharacter, List<Character> CharactersInBattle)
{
System.Random rnd = new System.Random();
HashSet<int> usedInitiatives = new HashSet<int>();
// Set up playerCharacter as the first item in the CharactersInBattle List
playerCharacter.initiative = rnd.Next(1, 21);
usedInitiatives.Add(playerCharacter.initiative);
// Set up initiatives for other characters
for (int i = 0; i < CharactersInBattle.Count; i++)
{
int initiative;
do
{
initiative = rnd.Next(1, 21);
} while (usedInitiatives.Contains(initiative));
CharactersInBattle[i].initiative = initiative;
usedInitiatives.Add(initiative);
}
}
void PerformEnemyAction(Character currentEnemy)
{
System.Random rnd = new System.Random();
int attackRoll = rnd.Next(1, 21);
int damageRoll = rnd.Next(1, currentEnemy.maxDamage);
//if currentcharacter != playerCharacter, then make an attack roll
//against playerCharacter's ArmorClass
if (attackRoll > controller.playerCharacter.armorClass)
{
controller.playerCharacter.currentHealth = controller.playerCharacter.currentHealth - damageRoll;
controller.LogStringWithReturn($"{currentEnemy.charName} hits you {"[" + attackRoll + "]"} for {damageRoll} damage!");
//Check if player died from that hit
if(isDead(controller.playerCharacter))
{
state = CombatState.LOST;
controller.LogStringWithReturn("The Eternal Night washes over you... \nYOU ARE DEAD\nGAME OVER");
GameOver();
}
}
else if (attackRoll < controller.playerCharacter.armorClass)
{
controller.LogStringWithReturn($"{currentEnemy.charName} misses its attack!");
}
else
{
controller.LogStringWithReturn($"You narrowly dodge {currentEnemy.charName}'s attack!");
}
}
bool isDead(Character character)
{
if (character.currentHealth <= 0)
{
return true;
}
else
{
return false;
}
}
void GameOver()
{
//Need to have something happen if you lose combat
//Disable text input or allow a restart command
//Maybe send them to a game over room???
}
void EndCombat()
{
controller.ResizeDisplayTextPostCombat();
}
}
I thought that it might help to change PerformEnemyAction into a coroutine as well. I tried that, but eventually went back to having it as the bug was still occurring.
To me it looks like you will never hit on the else statement because the if statement is asking if character != Controller.playerCharacter. You have a type mismatch, because the for each loop is only looping the Character Objects while the player character is of type 'PlayerCharacter'.
C# is a strongly typed language and you should aim to redesign your current approach that tries to declare a typeless var that has List/Array properties or else you are heading into a world of issues.
There are a lot of ways to do this, you could make a stack and pop objects from them, or you could also make an event system etc. I personally would give a 'CombatOrder' int variable on both the player character and character classes. Then both classes can have a method called "AssignInitialCombatOrder(int order)' and "ProgressCombatOrder()" This way you can justcalculate the order at combat state change, give them to each object, then have their update loops do nothing unless the combat order is 0 or something, then it resets back to end of combat order. No need for crazy yield or coroutines etc