/// <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> /// Creates and returns an instance of an AppliedStatus. /// </summary> /// <param name="applicator">The character from which the status originated.</param> /// <param name="statusBase">The status which the AppliedStatus is based off.</param> /// <returns>A wrapper around a status effect, containing modified values and a turn counter.</returns> private AppliedStatus CreateAppliedStatus(Character applicator, StatusEffect statusBase) { return(new AppliedStatus() { Applicator = applicator, BaseStatus = statusBase, TotalDamage = DamageCalculator.GetDamage(applicator, statusBase), HealAmount = DamageCalculator.GetHealing(applicator, statusBase), HealPercentage = DamageCalculator.GetHealingPercentage(applicator, statusBase), TurnsRemaining = statusBase.Duration, CritChance = applicator.CritChance + statusBase.CritChance, CritMultiplier = applicator.CritMultiplier + statusBase.CritMultiplier, StackCount = 1 }); }
/// <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 status instance, which will apply a status effect after a set amount of turns has passed. /// </summary> /// <param name="applicator">The character from which the spell originated.</param> /// <param name="statusBase">The status effect to use as the base for the delayed status.</param> /// <param name="target">A list of characters to apply to status effects to.</param> /// <param name="spellDelay">How many rounds until the buffs activate.</param> public void CreateDelayedStatus(Character applicator, StatusEffect statusBase, IReadOnlyList <int> targets, int spellDelay) { var delayedStatus = new DelayedStatus() { Applicator = applicator, BaseStatus = statusBase, TotalDamage = DamageCalculator.GetDamage(applicator, statusBase), HealAmount = DamageCalculator.GetHealing(applicator, statusBase), HealPercentage = DamageCalculator.GetHealingPercentage(applicator, statusBase), SpellDelay = spellDelay, CritChance = applicator.CritChance + statusBase.CritChance, CritMultiplier = applicator.CritMultiplier + statusBase.CritMultiplier, Targets = targets }; if (!_delayedStatuses.ContainsKey(applicator)) { _delayedStatuses[applicator] = new List <DelayedStatus>(); } _delayedStatuses[applicator].Add(delayedStatus); }
/// <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> /// 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) }); } }