/// <summary> /// Select a target for the spell from a list of candidates and cast the spell on it /// </summary> /// <param name="potentialTargets">The list of potential targets</param> public override void CastOn(List <UnitStack> potentialTargets) { if (potentialTargets.Count > 0) { // note: the spell doesn't work on holy units UnitStack toTarget = potentialTargets[0]; int qty = toTarget.GetTotalQty(); int candidateQty; for (int i = 1; i < potentialTargets.Count; i++) { candidateQty = potentialTargets[i].GetTotalQty(); if (toTarget.IsAffectedBy(this) || toTarget.GetUnitType().IsHoly() || (candidateQty > qty && !potentialTargets[i].IsAffectedBy(this) && !potentialTargets[i].GetUnitType().IsHoly())) { toTarget = potentialTargets[i]; qty = candidateQty; } } if (!toTarget.IsAffectedBy(this) && !toTarget.GetUnitType().IsHoly()) { toTarget.AffectBySpell(this); } } }
public void PlayExplosionAnimation() { if (!_explosionActivated) { FileLogger.Trace("COMBAT VIEW", "Playing explosion animation on " + _model.GetUnitType().GetName()); _explosion.gameObject.SetActive(true); _explosion.TimeIsUp += OnExplosionCompleted; _explosionActivated = true; _explosion.StartTimer(); } }
/// <summary> /// Handles defender selection and related animations. /// </summary> private void ProcessPhaseWithAttacks() { FileLogger.Trace("COMBAT VIEW", "ProcessPhaseWithAttacks"); // unit stacks could be created or destroyed - update the stack views UpdateUnitStackViews(); if (!_model.AreThereUnresolvedAttacks()) { _skipPhase = false; ProcessCombatTurn(); } else { // show all attacks UpdateAttackViews(); AttackRollResultsCollection currentAttackBatch = _model.SelectAttackRollResultsCollection(); if (currentAttackBatch != null) { FileLogger.Trace("COMBAT", "Selected attacks by " + currentAttackBatch.GetUnitStack().GetUnitType().GetName()); if (currentAttackBatch.GetUnitStack() != _currentAttacker) { SetNewStackForCurrentAttackView(currentAttackBatch.GetUnitStack()); // a new defender will be selected against a new attack _currentDefender = null; _skipStack = false; FileLogger.Trace("COMBAT VIEW", "ProcessPhaseWithAttacks: Setting skip stack to false"); } if (!_model.IsPlayerTurn()) { SelectAnNPCStackAsTarget(); } else { // if it's the player's turn // and he already selected a defending stack, resolve the attack if (_currentDefender != null && _currentDefender.GetTotalQty() > 0) { FileLogger.Trace("COMBAT", "Resolving an attack against " + _currentDefender.GetUnitType().GetName()); _model.ResolveCurrentAttack(false); } // else wait until the player will click on a stack view } } // else panic - there should be no case when there are unresolved attacks // but the model can't return an attack collection } }
private double GetEstimatedDamage(UnitStack attacker, UnitStack defender, Combat.TurnPhase phase) { int intPhase = (int)phase; if (attacker.GetAffectingSpells().Count == 0 && defender.GetAffectingSpells().Count == 0) { int attackerTypeId = attacker.GetUnitType().GetId(); if (_expectedDamage.ContainsKey(attackerTypeId)) { Dictionary <int, Dictionary <int, double> > attackerRecords = _expectedDamage[attackerTypeId]; int defenderTypeId = defender.GetUnitType().GetId(); if (attackerRecords.ContainsKey(defenderTypeId)) { Dictionary <int, double> defenderRecords = attackerRecords[defenderTypeId]; if (defenderRecords.ContainsKey(intPhase)) { return(defenderRecords[intPhase]); } } } } // "not found" return(-1); }
/// <summary> /// Select a unit stack from a list of existing ones and make an illusory clone of it /// </summary> /// <param name="existing">The list of existing unit stacks</param> public override UnitStack Create(List <UnitStack> existing) { if (existing.Count > 0) { UnitStack toClone = existing[0]; int qty = toClone.GetTotalQty(); int candidateQty; for (int i = 1; i < existing.Count; i++) { candidateQty = existing[i].GetTotalQty(); if (candidateQty > qty) { toClone = existing[i]; qty = candidateQty; } } UnitType illusion = new UnitType(toClone.GetUnitType()); illusion.SetShield(0); illusion.SetArmor(0); illusion.SetHitPoints(1); illusion.AddAttackQuality(AttackData.Quality.ILLUSORY); Unit mirrorImage = new Unit(illusion, toClone.GetTotalQty()); UnitStack stack = new UnitStack(mirrorImage, toClone.GetProvinceToRetreat()); stack.AffectBySpell(this); return(stack); } return(null); }
/// <summary> /// Calculate defender's armor value against the attack /// </summary> /// <param name="attack">Attack under consideration</param> /// <param name="defender">Defending unit stack</param> /// <param name="isCritical">Whether the attack was a critical success</param> /// <returns>Defender's armor value modified by attack's qualities</returns> public int CalculateArmorValue(Attack attack, UnitStack defender, bool isCritical) { // AP (armor piercing), GUNPOWDER and LIGHTNING attacks halve armor rating // FIRE attacks ignore armor // Critical hits ignore armor // STONE SKIN spell adds 2 to the armor value after the above rules were applied // ILLUSORY attacks ignore armor and ignore effects of STONE SKIN spell int result = defender.GetUnitType().GetArmor(); if (attack.IsArmorPiercing() || attack.IsGunpowderAttack() || attack.HasQuality(AttackData.Quality.LIGHTNING)) { result /= 2; } if (isCritical && IsUsingCriticals()) { result = 0; } if (attack.IsFireAttack()) { result = 0; } if (defender.IsAffectedBy("Stone Skin")) { result += 2; } if (attack.HasQuality(AttackData.Quality.ILLUSORY)) { result = 0; } return(result); }
/// <summary> /// Cleans up results of resolution /// </summary> private void ProcessAttackResolutionResults() { FileLogger.Trace("COMBAT VIEW", "ProcessAttackResolutionResults"); if (_attackViews[_currentAttacker].GetModel().Count == 0) { FileLogger.Trace("COMBAT VIEW", "Destroying " + _currentAttacker.GetUnitType().GetName() + "'s attacks view"); Destroy(_attackViews[_currentAttacker].gameObject); _attackViews.Remove(_currentAttacker); _currentAttacker = null; } else { FileLogger.Trace("COMBAT VIEW", "Updating " + _currentAttacker.GetUnitType().GetName() + "'s attacks view"); _attackViews[_currentAttacker].UpdateView(); } if (_currentDefender.GetTotalQty() == 0) { DestroyUnitStackView(_currentDefender); UpdateUnitStackViews(); _currentDefender = null; } else { FileLogger.Trace("COMBAT VIEW", "Updating " + _currentDefender.GetUnitType().GetName() + "'s stack view"); Dictionary <UnitStack, UnitStackView> stackViews = _attackerStackViews.ContainsKey(_currentDefender) ? _attackerStackViews : _defenderStackViews; stackViews[_currentDefender].UpdateView(); } // if the attacker is reset, reset the defender, too // because a different defender will likely to be selected // against a new attack if (_currentAttacker == null) { _currentDefender = null; } if (_attackResolutionView.gameObject.activeSelf) { _attackResolutionView.gameObject.SetActive(false); } _model.PerformEndOfCombatCheck(); if (!_model.IsCombatOver()) { ProcessCombatTurn(); } }
private void SetEstimatedDamage(UnitStack attacker, UnitStack defender, Combat.TurnPhase phase, double damage) { if (attacker.GetAffectingSpells().Count == 0 && defender.GetAffectingSpells().Count == 0) { int attackerTypeId = attacker.GetUnitType().GetId(); if (!_expectedDamage.ContainsKey(attackerTypeId)) { _expectedDamage[attackerTypeId] = new Dictionary <int, Dictionary <int, double> >(); } int defenderTypeId = defender.GetUnitType().GetId(); if (!_expectedDamage[attackerTypeId].ContainsKey(defenderTypeId)) { _expectedDamage[attackerTypeId][defenderTypeId] = new Dictionary <int, double>(); } _expectedDamage[attackerTypeId][defenderTypeId][(int)phase] = damage; } FileLogger.Trace("ESTIMATE", "Estimated damage of " + attacker.GetUnitType().GetName() + " vs " + defender.GetUnitType().GetName() + " during " + phase + " phase is " + damage); }
private void DestroyUnitStackView(UnitStack stack) { FileLogger.Trace("VIEW", "Destroying " + stack.GetUnitType().GetName() + "'s stack view"); bool isAttackerStackDestroyed = _attackerStackViews.ContainsKey(stack); Dictionary <UnitStack, UnitStackView> stackViews = isAttackerStackDestroyed ? _attackerStackViews : _defenderStackViews; stackViews[stack].ExplosionAnimationCompleted -= OnExplosionAnimationCompleted; stackViews[stack].StackSelected -= OnPlayerSelectedDefendingUnitStack; stackViews[stack].StackInspected -= OnStackInspected; stackViews[stack].StackInspectionEnded -= OnUnitTypeInspectionEnded; Destroy(stackViews[stack].gameObject); stackViews.Remove(stack); }
/// <summary> /// Estimate results of a unit stack attacking another during a specified turn phase /// </summary> /// <param name="attacker">Attacking unit stack</param> /// <param name="defender">Defending unit stack</param> /// <param name="phase">Turn phase</param> /// <returns>Mathematical expectation of the attack damage value</returns> public double EstimateStackAttacksDamage(UnitStack attacker, UnitStack defender, Combat.TurnPhase phase) { double result = GetEstimatedDamage(attacker, defender, phase); if (result >= 0) { return(result); } result = 0; int maxHP = defender.GetUnitType().GetHitPoints(); List <Attack> attacks = attacker.GetUnitType().GetAttacksForPhase(phase); for (int i = 0; i < attacks.Count; i++) { int numberOfAttacks = attacks[i].GetNumberOfAttacks(); for (int attackerPlusDie = 0; attackerPlusDie < _diceSides; attackerPlusDie++) { for (int attackerMinusDie = 0; attackerMinusDie < _diceSides; attackerMinusDie++) { AttackRollResult attackRollResult = CreateAnAttackRollResult(attacker, attacks[i], attackerPlusDie + 1, attackerMinusDie + 1); int defensiveSkill = CalculateDefensiveSkill(attackRollResult.Attack, defender); int shield = CalculateShieldValue(attackRollResult.Attack, defender); int armor = CalculateArmorValue(attackRollResult.Attack, defender, attackRollResult.IsCritical); for (int defenderPlusDie = 0; defenderPlusDie < _diceSides; defenderPlusDie++) { for (int defenderMinusDie = 0; defenderMinusDie < _diceSides; defenderMinusDie++) { // both defenderPlusDie and defenderMinusDie would add +1 to them, // so the +1s would cancel each other int defenseRoll = defenderPlusDie - defenderMinusDie; int totalDefense = defenseRoll + defensiveSkill + shield; result += numberOfAttacks * EstimateWeaponDamage(attackRollResult, defender, totalDefense, armor, maxHP); } } } } } result *= _damageScale; SetEstimatedDamage(attacker, defender, phase, result); return(result); }
private void OnPlayerSelectedDefendingUnitStack(object sender, EventArgs args) { if (_model.IsPlayerTurn()) { UnitStack stack = ((UnitStackView)sender).GetModel(); if (_attackViews.Count > 0) { _currentDefender = stack; _model.SetDefendingStack(stack); FileLogger.Trace("COMBAT VIEW", "Player selected " + _currentDefender.GetUnitType().GetName() + " as a defender"); Dictionary <UnitStack, UnitStackView> stackViews = _attackerStackViews.ContainsKey(_currentDefender) ? _attackerStackViews : _defenderStackViews; stackViews[_currentDefender].PlayExplosionAnimation(); } else { FileLogger.Error("COMBAT", "A defending unit stack is chosen, but there are no attacks to resolve."); } } }
/// <summary> /// Calculate defender's skill value against the attack /// </summary> /// <param name="attack">Attack under consideration</param> /// <param name="defender">Defending unit stack</param> /// <returns>Defensive skill modified by attack's qualities</returns> public int CalculateDefensiveSkill(Attack attack, UnitStack defender) { // RANGED attacks ignore defense skill // SKIRMISH or MAGIC attacks halve it int result = defender.GetUnitType().GetDefense(); if (attack.IsRangedAttack()) { result = 0; } else { if (attack.IsSkirmishAttack() || attack.HasQuality(AttackData.Quality.MAGIC)) { result /= 2; } } return(result); }
/// <summary> /// Calculate mathematical expectation of the attack damage value /// </summary> /// <param name="attackRollResult">Attack roll result object representing the attack</param> /// <param name="defender">Defending unit stack</param> /// <returns>Mathematical expectation of the attack damage value</returns> public int EstimateAttackDamage(AttackRollResult attackRoll, UnitStack defender) { int result = 0; double totalDamage = 0; int maxHP = defender.GetUnitType().GetHitPoints(); for (int attackerPlusDie = 0; attackerPlusDie < _diceSides; attackerPlusDie++) { for (int attackerMinusDie = 0; attackerMinusDie < _diceSides; attackerMinusDie++) { AttackRollResult attackRollResult = CreateAnAttackRollResult(attackRoll.UnitStack, attackRoll.Attack, attackerPlusDie + 1, attackerMinusDie + 1); int defensiveSkill = CalculateDefensiveSkill(attackRollResult.Attack, defender); int shield = CalculateShieldValue(attackRollResult.Attack, defender); int armor = CalculateArmorValue(attackRollResult.Attack, defender, attackRollResult.IsCritical); for (int defenderPlusDie = 0; defenderPlusDie < _diceSides; defenderPlusDie++) { for (int defenderMinusDie = 0; defenderMinusDie < _diceSides; defenderMinusDie++) { // both defenderPlusDie and defenderMinusDie would add +1 to them, // so the +1s would cancel each other int defenseRoll = defenderPlusDie - defenderMinusDie; int totalDefense = defenseRoll + defensiveSkill + shield; totalDamage += EstimateWeaponDamage(attackRollResult, defender, totalDefense, armor, maxHP); } } } } totalDamage *= _damageScale; result = Convert.ToInt32(Math.Floor(totalDamage)); totalDamage -= result; System.Random rando = new System.Random(); if (totalDamage > rando.NextDouble()) { result++; } return(result); }
/// <summary> /// Resolve combat /// Used by the game to resolve NPC-to-NPC battles /// and by Strategos to plan invasions /// </summary> /// <param name="useEstimates">Whether estimated results will be used or honest rolls will be made</param> public void ResolveCombat(bool useEstimates = false) { while (!IsCombatOver()) { PerformPhaseActions(); AttackRollResultsCollection currentAttackBatch = SelectAttackRollResultsCollection(); if (currentAttackBatch != null) { FileLogger.Trace("COMBAT", "Selected attacks by " + currentAttackBatch.GetUnitStack().GetUnitType().GetName()); UnitStack target = SelectDefendingStack(); if (target != null) { FileLogger.Trace("COMBAT", "Selected " + target.GetUnitType().GetName() + " as a target"); ResolveCurrentAttack(useEstimates); } else { PerformEndOfCombatCheck(); } } } }
/// <summary> /// Select the unit stack which has the lowest training cost /// </summary> /// <param name="stacks">Available unit stacks</param> /// <returns>Unit stack selected</returns> public UnitStack SelectCheapestUnit(List <UnitStack> stacks) { UnitStack result = null; int trainingCost = 0; for (int i = 0; i < stacks.Count; i++) { if (stacks[i].GetTotalQty() == 0) { FileLogger.Trace("TAI", stacks[i].GetUnitType().GetName() + " is empty"); continue; } if (result == null) { result = stacks[i]; trainingCost = stacks[i].GetUnitType().GetTrainingCost(); FileLogger.Trace("TAI", "Initial selection: " + stacks[i].GetUnitType().GetName() + ", training cost: " + trainingCost.ToString()); } else { int alternativeTrainingCost = stacks[i].GetUnitType().GetTrainingCost(); if (alternativeTrainingCost > 0 && alternativeTrainingCost < trainingCost) { result = stacks[i]; trainingCost = alternativeTrainingCost; FileLogger.Trace("TAI", "Current selection: " + stacks[i].GetUnitType().GetName() + ", training cost: " + trainingCost.ToString()); } else { FileLogger.Trace("TAI", stacks[i].GetUnitType().GetName() + " is more expensive to train: " + alternativeTrainingCost.ToString()); } } } if (result != null) { FileLogger.Trace("TAI", "Easy Level AI selected " + result.GetUnitType().GetName() + " as a target"); } return(result); }
/// <summary> /// Calculate defender's shield value against the attack /// </summary> /// <param name="attack">Attack under consideration</param> /// <param name="defender">Defending unit stack</param> /// <returns>Defender's shield value modified by attack's qualities</returns> public int CalculateShieldValue(Attack attack, UnitStack defender) { // Shields perform at 150% against SKIRMISH attacks // They perform at 200% against regular RANGED attacks // GUNPOWDER attacks ignore shields // MAGIC and LIGHTNING attacks halve the shield value // MAGIC SHIELD spell adds 2 to the shield value after the rules above were applied // ILLUSORY attacks ignore shields, including those created by MAGIC SHIELD spell int shieldRating = defender.GetUnitType().GetShield(); int result = shieldRating; if (attack.IsSkirmishAttack()) { result += shieldRating / 2; } if (attack.IsRangedAttack()) { result = 2 * shieldRating; } if (attack.HasQuality(AttackData.Quality.MAGIC) || attack.HasQuality(AttackData.Quality.LIGHTNING)) { result = shieldRating / 2; } if (attack.HasQuality(AttackData.Quality.GUNPOWDER)) { result = 0; } if (defender.IsAffectedBy("Magic Shield")) { result += 2; } if (attack.HasQuality(AttackData.Quality.ILLUSORY)) { result = 0; } return(result); }
/// <summary> /// Select the unit stack which is going to be cheapest to re-train, taking into account odds of successful defense /// </summary> /// <param name="stacks">Available unit stacks</param> /// <param name="attacker">Attacking enemy unit stack</param> /// <param name="phase">Turn phase of the combat</param> /// <returns>Unit stack selected</returns> public UnitStack SelectMinReplacementCostUnit(List <UnitStack> stacks, UnitStack attacker, Combat.TurnPhase phase) { UnitStack result = null; double replacementCost = double.MaxValue; double altervative; for (int i = 0; i < stacks.Count; i++) { if (stacks[i].GetTotalQty() > 0) { altervative = CombatHelper.Instance.EstimateStackAttacksDamage(attacker, stacks[i], phase) * stacks[i].GetUnitType().GetTrainingCost() / stacks[i].GetUnitType().GetHitPoints(); if (altervative < replacementCost) { replacementCost = altervative; result = stacks[i]; } } } if (result != null) { FileLogger.Trace("TAI", "Medium Level AI selected " + result.GetUnitType().GetName() + " as a target, expected replacement cost is " + replacementCost); } return(result); }
/// <summary> /// Get attacker's unit type /// </summary> /// <returns>Unit type of the attacker</returns> public UnitType GetUnitType() { return(UnitStack.GetUnitType()); }
/// <summary> /// Is the unit stack a valid target for the attack? /// </summary> /// <param name="target">Unit stack which is a potential target for the attack</param> /// <param name="attackRollResult">Attack roll result representing the attack</param> /// <returns>Whether the unit stack is a valid target for the attack</returns> private bool IsValidAttackTarget(UnitStack target, AttackRollResult attackRollResult) { if (_attackers.Contains(target) && _attackers.Contains(attackRollResult.UnitStack)) { FileLogger.Trace("COMBAT", "Both " + attackRollResult.UnitStack.GetUnitType().GetName() + " and " + target.GetUnitType().GetName() + " belong to the attacker"); return(false); } if (_defenders.Contains(target) && _defenders.Contains(attackRollResult.UnitStack)) { FileLogger.Trace("COMBAT", "Both " + attackRollResult.UnitStack.GetUnitType().GetName() + " and " + target.GetUnitType().GetName() + " belong to the defender"); return(false); } if (target.GetTotalQty() == 0) { FileLogger.Trace("COMBAT", "No defenders left in this stack"); return(false); } return(true); }
/// <summary> /// Set the defending unit stack /// </summary> /// <param name="stack">Unit stack that will be defending against attacks</param> public void SetDefendingStack(UnitStack stack) { _currentTarget = stack; FileLogger.Trace("COMBAT", "SetDefendingStack: set " + _currentTarget.GetUnitType().GetName() + " as a target."); }
/// <summary> /// Stops "object selected" animation and probably destroys the current attacks view /// Starts "object selected" animation on the new one /// </summary> private void SelectAnNPCStackAsTarget() { FileLogger.Trace("COMBAT VIEW", "SelectAnNPCStackAsTarget"); UnitStack target = _model.SelectDefendingStack(); if (target != null) { FileLogger.Trace("COMBAT", "Selected " + target.GetUnitType().GetName() + " as a target"); if (_currentDefender != target) { _currentDefender = target; // cover the case when the game selects // an NPC defender who's offscreen bool areProcessingAttackers = false; List <UnitStack> stacks = _model.GetUnitStacks(areProcessingAttackers); int index = stacks.FindIndex(a => a == _currentDefender); if (index == -1) { areProcessingAttackers = true; stacks = _model.GetUnitStacks(areProcessingAttackers); index = stacks.FindIndex(a => a == _currentDefender); } Dictionary <UnitStack, UnitStackView> stackViews = areProcessingAttackers ? _attackerStackViews : _defenderStackViews; // these variables are defined here so that they can be used in an error message below int offset = -1; int numberOfSpawnPoints = -1; if (!stackViews.ContainsKey(_currentDefender)) { numberOfSpawnPoints = areProcessingAttackers ? _attackersSpawnPoints.Length : _defendersSpawnPoints.Length; offset = index >= numberOfSpawnPoints ? index + 1 - numberOfSpawnPoints : 0; FileLogger.Trace("COMBAT VIEW", "Current defender: " + _currentDefender.GetUnitType().GetName() + ", index = " + index + ", number of spawn points: " + numberOfSpawnPoints + ", offset: " + offset + ")"); if (areProcessingAttackers) { _attackerStackViewsOffset = offset; } else { _defenderStackViewsOffset = offset; } UpdateUnitStackViews(); } if (stackViews.ContainsKey(_currentDefender)) { // the attack will be resoved after the explosion animation plays out stackViews[_currentDefender].PlayExplosionAnimation(); } else { FileLogger.Error("COMBAT", "Selected " + _currentDefender.GetUnitType().GetName() + " as a target, but it doesn't have a view! (index = " + index + ", number of spawn points: " + numberOfSpawnPoints + ", offset: " + offset + ")"); } } else { FileLogger.Trace("COMBAT", "SelectAnNPCStackAsTarget: resolving the current attack"); // explosion animation already played for this defender - // go ahead and resolve the attack _model.ResolveCurrentAttack(false); } } // else panic - if it's not the player's turn select a defender, the model should not return null }
public void SetModel(UnitStack unitStack) { _model = unitStack; _model.WoundCheckMade += OnWoundCheckMade; _unitImage.material.mainTexture = SpriteCollectionManager.GetTextureByName(_model.GetUnitType().GetName()); _mouseListener.MouseClickDetected += OnStackSelected; _mouseOverListener.MouseOverDetected += OnStackInspected; _mouseOverListener.MouseExitDetected += OnStackInspectionEnd; _explosion.gameObject.SetActive(false); _healing.gameObject.SetActive(false); UpdateView(); }