Example #1
0
 public static void Init()
 {
     if (_processSystems == null)
     {
         _processSystems = new List <ProcessSystemBase>();
         string data =
             File.ReadAllText(Application.streamingAssetsPath + "/Data/ProcessSystemData/ProcessSystems.xml");
         XDocument       database = XDocument.Parse(data);
         List <XElement> events   = database.Root.Elements("Event").ToList();
         foreach (XElement e in events)
         {
             foreach (XElement listner in e.Elements("Listner"))
             {
                 ProcessSystemBase system =
                     Activator.CreateInstance(Type.GetType(_systemNamespace + listner.Value)) as
                     ProcessSystemBase;
                 if (system != null)
                 {
                     XAttribute messengerAttribute = e.Attribute("Messenger");
                     if ((messengerAttribute != null) && (messengerAttribute.Value == "CombatantMessenger"))
                     {
                         Messenger <ICombatant> .AddListener(e.Attribute("Type").Value, system.Process);
                     }
                     else
                     {
                         CombatMessenger.AddListener(e.Attribute("Type").Value, system.Process);
                     }
                     _processSystems.Add(system);
                 }
             }
         }
     }
 }
        /// <summary>
        /// Calculates healing and damage, then checks and applies any critical strikes, then applies healing and damage.
        /// </summary>
        /// <param name="actor">The character applying the healing and damage.</param>
        /// <param name="action">The action being performed.</param>
        /// <param name="targets">The list of characters the action is targeting.</param>
        private void ApplyHealingAndDamage(Character actor, ActionBase action, IReadOnlyList <Character> targets)
        {
            var damageTypes = DamageCalculator.GetDamage(actor, action);

            int[] damage = damageTypes.AsArray();
            var   postHealthChangedDict = new Dictionary <int, int>();
            var   changeAmount          = new Dictionary <int, int>();
            var   loggableData          = new List <KeyValuePair <string, int> >();
            var   preHealthChangedDict  = new Dictionary <int, int>();

            for (int i = 0; i < targets.Count(); i++)
            {
                int startHealth = targets[i].CurrentHealth;
                preHealthChangedDict.Add(targets[i].Id, startHealth);
                // Calculate and apply healing
                int percentHealAmount  = targets[i].CurrentMaxHealth * DamageCalculator.GetHealingPercentage(actor, action);
                int modifiedHealAmount = DamageCalculator.GetHealing(actor, action);
                int totalDamage        = DamageCalculator.GetTotalDamage(damageTypes, targets[i]);

                // Determine if a critical happened
                int totalCritChance = action.CritChance > 0 ? action.CritChance + actor.CritChance : 0;
                int rand            = _random.Next(1, 101);
                if (totalCritChance >= rand)
                {
                    int multiplier = action.CritMultiplier + actor.CritMultiplier + 100;
                    modifiedHealAmount = modifiedHealAmount * multiplier / 100;
                    totalDamage        = totalDamage * multiplier / 100;
                }

                _characterController.ModifyCurrentHealth(targets[i], percentHealAmount);
                _characterController.ModifyCurrentHealth(targets[i], modifiedHealAmount);
                _characterController.ModifyCurrentHealth(targets[i], totalDamage);
                _threatController.ApplyThreat(actor,
                                              targets[i],
                                              percentHealAmount + modifiedHealAmount + totalDamage,
                                              action.Threat,
                                              action.ThreatMultiplier);

                // Prepare event data
                int healthChange = targets[i].CurrentHealth - startHealth;
                changeAmount.Add(targets[i].Id, healthChange);
                loggableData.Add(new KeyValuePair <string, int>(targets[i].Name, healthChange));
                postHealthChangedDict.Add(targets[i].Id, targets[i].CurrentHealth);
            }

            if (changeAmount.Values.Any(val => val != 0))
            {
                string logMessage = CombatMessenger.GetHealthChangedMessage(actor.Name,
                                                                            action.Name,
                                                                            loggableData);

                CharactersHealthChanged?.Invoke(this, new CharactersHealthChangedEventArgs()
                {
                    PostCharactersChanged = postHealthChangedDict,
                    PreCharactersChanged  = preHealthChangedDict,
                    ChangeAmount          = changeAmount,
                    LogMessage            = logMessage
                });
            }
        }
        /// <summary>
        /// Applies a status effect to a group of characters.
        /// </summary>
        /// <param name="applicator">The character applying the status effect.</param>
        /// <param name="status">The status effect being applied.</param>
        /// <param name="characters">The characters the status effect is being applied to.</param>
        public void ApplyStatus(Character applicator, StatusEffect status, IEnumerable <Character> characters)
        {
            foreach (var character in characters)
            {
                ApplyStatus(applicator, status, character, false);
            }

            StatusEffectApplied?.Invoke(this, new StatusEffectAppliedEventArgs()
            {
                AffectedCharacterIds = new List <int>(characters.Select(chr => chr.Id)),
                LogMessage           = CombatMessenger.GetAffectedByStatusMessage(status.Name, characters.Select(chr => chr.Name).ToList())
            });
        }
        /// <summary>
        /// Checks a list of characters if any have died. If a character has died, invokes the CharacterDied event.
        /// </summary>
        /// <param name="targets">The list of characters to check.</param>
        private void CheckForDeadTargets(IReadOnlyList <Character> targets)
        {
            var deadCharacters = targets.Where(target => target.CurrentHealth <= 0).ToList();

            if (deadCharacters.Count() > 0)
            {
                var logMessage = CombatMessenger.GetCharactersDiedMessage(deadCharacters.Select(chr => chr.Name).ToList());

                CharactersDied?.Invoke(this, new CharactersDiedEventArgs()
                {
                    DyingCharacters = deadCharacters.ToList(),
                    LogMessage      = logMessage
                });
            }
        }
        /// <summary>
        /// Applies healing then damage from a delayed action.
        /// </summary>
        /// <param name="action">The delayed action causing the healing and damage.</param>
        private void ApplyHealingAndDamage(DelayedAction action)
        {
            var targets = new List <Character>(AllCharacters.Where(
                                                   character => IsTargetableCharacter(character, action.BaseAction, action.Targets)));

            var postHealthChangedDict = new Dictionary <int, int>();
            var changeAmount          = new Dictionary <int, int>();
            var loggableData          = new List <KeyValuePair <string, int> >();
            var preHealthChangedDict  = new Dictionary <int, int>();

            for (int i = 0; i < targets.Count(); i++)
            {
                int startingHealth = targets[i].CurrentHealth;
                preHealthChangedDict.Add(targets[i].Id, startingHealth);
                int totalDamage       = DamageCalculator.GetTotalDamage(action.TotalDamage, targets[i]);
                int percentHealAmount = targets[i].CurrentMaxHealth * action.HealPercentage;

                _characterController.ModifyCurrentHealth(targets[i], percentHealAmount);
                _characterController.ModifyCurrentHealth(targets[i], action.HealAmount);
                _characterController.ModifyCurrentHealth(targets[i], totalDamage);
                _threatController.ApplyThreat(action.Actor,
                                              targets[i],
                                              percentHealAmount + action.HealAmount + totalDamage,
                                              action.BaseAction.Threat,
                                              action.BaseAction.ThreatMultiplier);
                int modifiedHealth = targets[i].CurrentHealth - startingHealth;
                changeAmount.Add(targets[i].Id, modifiedHealth);
                loggableData.Add(new KeyValuePair <string, int>(targets[i].Name, modifiedHealth));
                postHealthChangedDict.Add(targets[i].Id, targets[i].CurrentHealth);
            }

            if (changeAmount.Values.Any(val => val != 0))
            {
                string logMessage = CombatMessenger.GetHealthChangedMessage(action.Actor.Name,
                                                                            action.BaseAction.Name,
                                                                            loggableData);

                CharactersHealthChanged?.Invoke(this, new CharactersHealthChangedEventArgs()
                {
                    PostCharactersChanged = postHealthChangedDict,
                    PreCharactersChanged  = preHealthChangedDict,
                    ChangeAmount          = changeAmount,
                    LogMessage            = logMessage
                });
            }

            CheckForDeadTargets(targets);
        }
        /// <summary>
        /// Creates a delayed action that has it's effects applied after an amount of rounds has passed.
        /// </summary>
        /// <param name="actor">The character performing the action.</param>
        /// <param name="action">The action being performed.</param>
        /// <param name="targets">The list of target positions the action targets.</param>
        private void CreateDelayedAction(Character actor, ActionBase action, IReadOnlyList <int> targets)
        {
            if (!_delayedActions.ContainsKey(actor))
            {
                _delayedActions[actor] = new List <DelayedAction>();
            }

            DamageTypes totalDamage       = DamageCalculator.GetDamage(actor, action);
            int         totalHeal         = DamageCalculator.GetHealing(actor, action);
            int         percentageHealing = DamageCalculator.GetHealingPercentage(actor, action);

            int rand            = _random.Next(1, 101);
            int totalCritChance = action.CritChance > 0 ? action.CritChance + actor.CritChance : 0;

            if (totalCritChance > rand)
            {
                int critMultiplier = actor.CritMultiplier + action.CritMultiplier + 100;
                totalDamage = totalDamage * critMultiplier / 100;
                totalHeal   = totalHeal * critMultiplier / 100;
            }

            _delayedActions[actor].Add(new DelayedAction()
            {
                Actor          = actor,
                BaseAction     = action,
                TotalDamage    = totalDamage,
                HealAmount     = totalHeal,
                HealPercentage = percentageHealing,
                TurnsRemaining = action.Delay,
                Targets        = targets
            });

            DelayedActionBeginChannel?.Invoke(this, new CombatLoggableEventArgs()
            {
                LogMessage = CombatMessenger.GetBeginChannelMessage(actor.Name, action.Name)
            });
        }
        /// <summary>
        /// Applies damage and healing from status effects affecting a character at the start of its turn.
        /// If there are no turns remaining on the status, the status is removed. If the character dies from
        /// status effect damage, the character died event is invoked.
        /// </summary>
        /// <param name="character">The character starting its turn.</param>
        private void StartOfTurnEffects(Character character)
        {
            var removeStatuses = new List <AppliedStatus>();
            int startingHealth = character.CurrentHealth;
            int totalDamage    = 0;

            // Calculate damage and apply healing from each status effect
            foreach (var status in _appliedStatuses[character])
            {
                int damage = DamageCalculator.GetTotalDamage(status.TotalDamage, character);
                totalDamage += damage;
                int healAmount  = status.HealAmount;
                int percentHeal = character.CurrentMaxHealth * status.HealPercentage / 100;
                if (_random.Next(1, 101) <= status.CritChance)
                {
                    totalDamage = totalDamage * status.CritMultiplier / 100;
                    healAmount  = healAmount * status.CritMultiplier / 100;
                }

                character.CurrentHealth += percentHeal;
                character.CurrentHealth += healAmount;

                _threatController.ApplyThreat(status.Applicator,
                                              character,
                                              damage + healAmount + percentHeal,
                                              status.BaseStatus.Threat,
                                              status.BaseStatus.ThreatMultiplier);

                if (character.CurrentHealth > character.CurrentMaxHealth)
                {
                    character.CurrentHealth = character.CurrentMaxHealth;
                }
                status.TurnsRemaining--;
                if (status.TurnsRemaining == 0)
                {
                    removeStatuses.Add(status);
                }
            }
            character.CurrentHealth += totalDamage;

            int modifiedHealth = character.CurrentHealth - startingHealth;
            var names          = _appliedStatuses[character].Select(status => status.BaseStatus.Name).ToList();

            if (modifiedHealth != 0)
            {
                CharactersHealthChanged?.Invoke(this, new CharactersHealthChangedEventArgs()
                {
                    PostCharactersChanged = new Dictionary <int, int>()
                    {
                        { character.Id, character.CurrentHealth }
                    },
                    PreCharactersChanged = new Dictionary <int, int>()
                    {
                        { character.Id, startingHealth }
                    },
                    ChangeAmount = new Dictionary <int, int>()
                    {
                        { character.Id, modifiedHealth }
                    },
                    LogMessage = CombatMessenger.GetHealthChangedByStatusMessage(names, character.Name, modifiedHealth)
                });
            }

            var statusNames = removeStatuses.Select(st => st.BaseStatus.Name).ToList();

            // If a status is queued for removal, remove from the character's buff and debuff lists
            foreach (var status in removeStatuses)
            {
                if (status.BaseStatus.IsDebuff)
                {
                    character.Debuffs.Remove(status.BaseStatus);
                }
                else
                {
                    character.Buffs.Remove(status.BaseStatus);
                }
                RemoveStatusEffects(status, character);
            }
            _appliedStatuses[character].RemoveAll(status => removeStatuses.Contains(status));

            if (removeStatuses.Any())
            {
                StatusEffectsRemoved?.Invoke(this, new CombatLoggableEventArgs()
                {
                    LogMessage = CombatMessenger.GetRemoveStatusMessage(statusNames, character.Name)
                });
            }

            // Invoke characters dying event if a character died as a result of this status effect.
            if (character.CurrentHealth <= 0)
            {
                character.CurrentHealth = 0;
                RemoveAllStatuses(character);
                var characters = new List <Character>()
                {
                    character
                };
                CharactersDied?.Invoke(this, new CharactersDiedEventArgs()
                {
                    DyingCharacters = characters,
                    LogMessage      = CombatMessenger.GetCharactersDiedMessage(characters.Select(chr => chr.Name).ToList())
                });
            }
        }
        /// <summary>
        /// Applys a delayed status effect to a group of characters.
        /// </summary>
        /// <param name="status">The delayed status to apply.</param>
        private void ApplyStatus(DelayedStatus status)
        {
            var livingTargets = new List <Character>(
                AllCharacters.Where(
                    character => status.Targets.Contains(character.Position)));

            livingTargets.RemoveAll(target => target.CurrentHealth <= 0);

            foreach (var target in livingTargets)
            {
                if (!_appliedStatuses.ContainsKey(target))
                {
                    _appliedStatuses[target] = new List <AppliedStatus>();
                }
                // If the the same type of status is already on a character
                if (_appliedStatuses[target].Any(applied => applied.BaseStatus == status.BaseStatus))
                {
                    var matchingStatus = _appliedStatuses[target].First(applied => applied.BaseStatus == status.BaseStatus);
                    // If the status is stackable, refresh the duration and apply another layer of effects
                    if (status.BaseStatus.Stackable && matchingStatus.StackCount < status.BaseStatus.StackSize)
                    {
                        matchingStatus.Applicator      = status.Applicator;
                        matchingStatus.TurnsRemaining  = status.BaseStatus.Duration;
                        matchingStatus.TotalDamage    += status.TotalDamage;
                        matchingStatus.HealAmount     += status.HealAmount;
                        matchingStatus.HealPercentage += status.HealPercentage;
                        ApplyStatusEffects(status.BaseStatus, target);
                        matchingStatus.StackCount++;
                    }
                    // If the status is stackable but has reached its stack limit, refresh the duration only
                    else if (status.BaseStatus.Stackable)
                    {
                        matchingStatus.Applicator     = status.Applicator;
                        matchingStatus.TurnsRemaining = status.BaseStatus.Duration;
                    }
                    // If the status isn't stackable, refresh the duration and reset the damage
                    else
                    {
                        matchingStatus.Applicator     = status.Applicator;
                        matchingStatus.TurnsRemaining = status.BaseStatus.Duration;
                        matchingStatus.TotalDamage    = status.TotalDamage;
                        matchingStatus.HealAmount     = status.HealAmount;
                        matchingStatus.HealPercentage = status.HealPercentage;
                    }
                }
                // Create and apply a new status effect on a character
                else
                {
                    ApplyStatusEffects(status.BaseStatus, target);
                    _appliedStatuses[target].Add(CreateAppliedStatus(status));
                    if (status.BaseStatus.IsDebuff)
                    {
                        target.Debuffs.Add(status.BaseStatus);
                    }
                    else
                    {
                        target.Buffs.Add(status.BaseStatus);
                    }
                }
            }

            StatusEffectApplied?.Invoke(this, new StatusEffectAppliedEventArgs()
            {
                AffectedCharacterIds = new List <int>(livingTargets.Select(chr => chr.Id)),
                LogMessage           = CombatMessenger.GetAffectedByStatusMessage(status.BaseStatus.Name,
                                                                                  livingTargets.Select(target => target.Name).ToList())
            });
        }
        /// <summary>
        /// Applies a status effect on a character.
        /// </summary>
        /// <param name="applicator">The character that is applying the status effect.</param>
        /// <param name="status">The status effect being applied.</param>
        /// <param name="character">The character the status effect is being applied on.</param>
        public void ApplyStatus(Character applicator, StatusEffect status, Character character, bool invokeAppliedStatusEvent = true)
        {
            if (!_appliedStatuses.ContainsKey(character))
            {
                _appliedStatuses[character] = new List <AppliedStatus>();
            }
            // If the the same type of status is already on a character
            if (_appliedStatuses[character].Any(applied => applied.BaseStatus == status))
            {
                var matchingStatus = _appliedStatuses[character].First(applied => applied.BaseStatus == status);
                // If the status is stackable, refresh the duration and apply another layer of effects
                if (status.Stackable && matchingStatus.StackCount < status.StackSize)
                {
                    matchingStatus.TurnsRemaining  = status.Duration;
                    matchingStatus.TotalDamage    += DamageCalculator.GetDamage(applicator, status);
                    matchingStatus.HealAmount     += DamageCalculator.GetHealing(applicator, status);
                    matchingStatus.HealPercentage += DamageCalculator.GetHealingPercentage(applicator, status);
                    matchingStatus.CritChance      = status.CritChance + character.CritChance;
                    matchingStatus.CritMultiplier  = status.CritMultiplier + character.CritMultiplier;
                    ApplyStatusEffects(status, character);
                    matchingStatus.StackCount++;
                }
                // If the status is stackable but has reached its stack limit, refresh the duration only
                else if (status.Stackable)
                {
                    matchingStatus.TurnsRemaining = status.Duration;
                }
                // If the status isn't stackable, refresh the duration and reset the damage
                else
                {
                    matchingStatus.TurnsRemaining = status.Duration;
                    matchingStatus.TotalDamage    = DamageCalculator.GetDamage(applicator, status);
                    matchingStatus.HealAmount     = DamageCalculator.GetHealing(applicator, status);
                    matchingStatus.HealPercentage = DamageCalculator.GetHealingPercentage(applicator, status);
                    matchingStatus.CritChance     = status.CritChance + character.CritChance;
                    matchingStatus.CritMultiplier = status.CritMultiplier + character.CritMultiplier;
                }
            }
            // Create and apply a new status effect on a character
            else
            {
                ApplyStatusEffects(status, character);
                _appliedStatuses[character].Add(CreateAppliedStatus(applicator, status));
                if (status.IsDebuff)
                {
                    character.Debuffs.Add(status);
                }
                else
                {
                    character.Buffs.Add(status);
                }
            }

            if (invokeAppliedStatusEvent)
            {
                StatusEffectApplied?.Invoke(this, new StatusEffectAppliedEventArgs()
                {
                    AffectedCharacterIds = new List <int>()
                    {
                        character.Id
                    },
                    LogMessage = CombatMessenger.GetAffectedByStatusMessage(status.Name, character.Name)
                });
            }
        }
        /// <summary>
        /// Performs a character action.
        /// </summary>
        /// <param name="commandType">The type of command that is being performed, such as Attack or Spells.</param>
        /// <param name="category">The category of the action being performed, may be left blank if the action has no categories.</param>
        /// <param name="index">The index of the action being performed.</param>
        /// <param name="targetPosition">The target position of the action being performed.</param>
        public void StartAction(Commands commandType, string category, int index, int targetPosition)
        {
            bool                isInvalidAction = false;
            ActionBase          action;
            IReadOnlyList <int> targets;

            switch (commandType)
            {
            case Commands.Attack:
                action          = DisplayManager.GetActionsFromCategory <Attack>(commandType, category)[index];
                isInvalidAction = !IsValidTarget(action, targetPosition);
                if (!isInvalidAction)
                {
                    targets = CombatTargeter.GetTranslatedTargetPositions(action.TargetPositions,
                                                                          action.CenterOfTargetsPosition,
                                                                          action.CanSwitchTargetPosition,
                                                                          targetPosition);
                    _actionController.StartAction(CombatStateHandler.CurrentRoundOrder[0], action, targets);
                }
                break;

            case Commands.Spells:
                action          = DisplayManager.GetActionsFromCategory <Spell>(commandType, category)[index];
                isInvalidAction = !IsValidTarget(action, targetPosition);
                if (!isInvalidAction)
                {
                    targets = CombatTargeter.GetTranslatedTargetPositions(action.TargetPositions,
                                                                          action.CenterOfTargetsPosition,
                                                                          action.CanSwitchTargetPosition,
                                                                          targetPosition);
                    _actionController.StartAction(CombatStateHandler.CurrentRoundOrder[0], action, targets);
                }
                break;

            case Commands.Items:
                var item = DisplayManager.GetConsumablesFromCategory(category)[index];
                isInvalidAction = !IsValidTarget(item.ItemSpell, targetPosition);
                if (!isInvalidAction)
                {
                    targets = CombatTargeter.GetTranslatedTargetPositions(item.ItemSpell.TargetPositions,
                                                                          item.ItemSpell.CenterOfTargetsPosition,
                                                                          item.ItemSpell.CanSwitchTargetPosition,
                                                                          targetPosition);
                    _actionController.StartAction(CombatStateHandler.CurrentRoundOrder[0], item.ItemSpell, targets);
                    _consumablesHandler.UseConsumable(item, CombatStateHandler.CurrentRoundOrder[0]);
                }
                break;

            case Commands.Wait:
                if (CombatStateHandler.CurrentRoundOrder.Count() == 1)
                {
                    isInvalidAction = true;
                }
                else
                {
                    CombatStateHandler.BeginWait();
                    CharacterBeginWait?.Invoke(this, new CombatLoggableEventArgs()
                    {
                        LogMessage = CombatMessenger.GetBeginWaitMessage(CombatStateHandler.CurrentRoundOrder[0].Name)
                    });
                }
                break;

            default:
                isInvalidAction = true;
                break;
            }

            if (!isInvalidAction)
            {
                EndTurn();
            }
        }