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(); } }