private void ShootIfReasonable(BattleSoldier soldier, bool isMoving) { if (soldier.RangedWeapons.Count == 0) { return; } float range = _grid.GetNearestEnemy(soldier.Soldier.Id, out int closestEnemyId); BattleSquad oppSquad = _opposingSoldierIdSquadMap[closestEnemyId]; BattleSoldier target = oppSquad.GetRandomSquadMember(); range = _grid.GetDistanceBetweenSoldiers(soldier.Soldier.Id, target.Soldier.Id); // decide whether to shoot or aim Tuple <float, float, RangedWeapon> weaponProfile = ShouldShootAtRange(soldier, target, range, isMoving); if (weaponProfile.Item3 != null) { int shotsToFire = CalculateShotsToFire(weaponProfile.Item3, weaponProfile.Item1, weaponProfile.Item2); _shootActionBag.Add(new ShootAction(soldier, weaponProfile.Item3, target, range, shotsToFire, isMoving, _woundResolutionBag, _log)); } else if (!isMoving) { // aim with longest ranged weapon _shootActionBag.Add(new AimAction(soldier, target, soldier.EquippedRangedWeapons.OrderByDescending(w => w.Template.MaximumRange).First(), _log)); } }
private void AddChargeActionsHelper(BattleSoldier soldier, int closestEnemyId, Tuple <int, int> currentPosition, float distance, BattleSquad oppSquad, Tuple <int, int> newPos) { Tuple <int, int> move = new Tuple <int, int>(newPos.Item1 - currentPosition.Item1, newPos.Item2 - currentPosition.Item2); float distanceSq = ((move.Item1 * move.Item1) + (move.Item2 * move.Item2)); float moveSpeed = soldier.GetMoveSpeed(); if (distance > moveSpeed + 1) { // we can't make it to an enemy in one move // soldier can't get there in one move, advance as far as possible Tuple <int, int> realMove = CalculateMovementAlongLine(move, moveSpeed); AddMoveAction(soldier, moveSpeed, realMove); // should the soldier shoot along the way? ShootIfReasonable(soldier, true); } else if (soldier.EquippedMeleeWeapons.Count == 0 && soldier.MeleeWeapons.Count > 0) { soldier.CurrentSpeed = 0; _shootActionBag.Add(new ReadyMeleeWeaponAction(soldier, soldier.MeleeWeapons[0])); } else { Debug.Log(soldier.Soldier.Name + " charging " + moveSpeed.ToString("F0")); soldier.CurrentSpeed = moveSpeed; _grid.ReserveSpace(newPos); _moveActionBag.Add(new MoveAction(soldier, _grid, newPos, _moveResolutionBag)); BattleSoldier target = oppSquad.Soldiers.Single(s => s.Soldier.Id == closestEnemyId); _meleeActionBag.Add(new MeleeAttackAction(soldier, target, soldier.MeleeWeapons.Count == 0 ? null : soldier.EquippedMeleeWeapons[0], distance >= 2, _woundResolutionBag, _log)); } }
private void AddStandingActionsToBag(BattleSoldier soldier) { if (soldier.RangedWeapons.Count == 0) { Debug.Log("ISoldier with no ranged weapons just standing around"); } // do we have a ranged weapon equipped else if (soldier.EquippedRangedWeapons.Count == 0 && soldier.RangedWeapons.Count > 0) { AddEquipRangedWeaponActionToBag(soldier); } // determine if soldier was already aiming and the target is still around and not in a melee else if (soldier.Aim != null && _opposingSoldierIdSquadMap.ContainsKey(soldier.Aim.Item1.Soldier.Id) && !soldier.Aim.Item1.IsInMelee) { // if the aim cannot be improved, go ahead and shoot if (soldier.Aim.Item3 == 2) { float range = _grid.GetDistanceBetweenSoldiers(soldier.Soldier.Id, soldier.Aim.Item1.Soldier.Id); Tuple <float, float> effectEstimate = EstimateHitAndDamage(soldier, soldier.Aim.Item1, soldier.Aim.Item2, range, soldier.Aim.Item2.Template.Accuracy + 3); int shotsToFire = CalculateShotsToFire(soldier.Aim.Item2, effectEstimate.Item1, effectEstimate.Item2); soldier.CurrentSpeed = 0; _shootActionBag.Add(new ShootAction(soldier, soldier.Aim.Item2, soldier.Aim.Item1, range, shotsToFire, false, _woundResolutionBag, _log)); } else { // the aim can be improved // current aim bonus is 1 for all-out attack, plus weapon accuracy, plus aim float range = _grid.GetDistanceBetweenSoldiers(soldier.Soldier.Id, soldier.Aim.Item1.Soldier.Id); float currentModifiers = soldier.Aim.Item2.Template.Accuracy + soldier.Aim.Item3 + 1; // item1 is the pre-roll to-hit total; item2 is the expected ratio of damage to con, so 1 is a potential killshot Tuple <float, float> resultEstimate = EstimateHitAndDamage(soldier, soldier.Aim.Item1, soldier.Aim.Item2, range, currentModifiers); // it's about to attack, go ahead and shoot, you may not get another chance if (soldier.Aim.Item1.GetMoveSpeed() > range // there's a good chance of both hitting and killing, go ahead and shoot now || (resultEstimate.Item2 >= 1 && resultEstimate.Item1 >= 6.66f)) { int shotsToFire = CalculateShotsToFire(soldier.Aim.Item2, resultEstimate.Item1, resultEstimate.Item2); soldier.CurrentSpeed = 0; _shootActionBag.Add(new ShootAction(soldier, soldier.Aim.Item2, soldier.Aim.Item1, range, shotsToFire, false, _woundResolutionBag, _log)); } else { // keep aiming soldier.CurrentSpeed = 0; _shootActionBag.Add(new AimAction(soldier, soldier.Aim.Item1, soldier.Aim.Item2, _log)); } } } // need to aim or shoot at a new target else { soldier.CurrentSpeed = 0; ShootIfReasonable(soldier, false); } }
private void AddRetreatingActionsToBag(BattleSoldier soldier, BattleSquad soldierSquad) { float moveSpeed = soldier.GetMoveSpeed(); int newY = (int)(soldierSquad.IsPlayerSquad ? -moveSpeed : moveSpeed); AddMoveAction(soldier, moveSpeed, new Tuple <int, int>(0, newY)); // determine if soldier will shoot as he falls back ShootIfReasonable(soldier, true); }
private void AddMoveAction(BattleSoldier soldier, float moveSpeed, Tuple <int, int> line) { Tuple <int, int> desiredMove = CalculateMovementAlongLine(line, moveSpeed); Tuple <int, int> newLocation = new Tuple <int, int>(soldier.Location.Item1 + desiredMove.Item1, soldier.Location.Item2 + desiredMove.Item2); if (_grid.IsSpaceReserved(newLocation) || !_grid.IsEmpty(newLocation)) { newLocation = FindBestLocation(soldier.Location, newLocation, moveSpeed); } soldier.CurrentSpeed = moveSpeed; _grid.ReserveSpace(newLocation); _moveActionBag.Add(new MoveAction(soldier, _grid, newLocation, _moveResolutionBag)); }
private Tuple <float, float> EstimateHitAndDamage(BattleSoldier soldier, BattleSoldier target, RangedWeapon weapon, float range, float moveAndAimMod) { float sizeMod = BattleModifiersUtil.CalculateSizeModifier(target.Soldier.Size); float armor = target.Armor.Template.ArmorProvided; float con = target.Soldier.Constitution; float expectedDamage = CalculateExpectedDamage(weapon, range, armor, con); float rangeMod = BattleModifiersUtil.CalculateRangeModifier(range, target.CurrentSpeed); float rofMod = BattleModifiersUtil.CalculateRateOfFireModifier(weapon.Template.RateOfFire); float weaponSkill = soldier.Soldier.GetTotalSkillValue(weapon.Template.RelatedSkill); float total = weaponSkill + rofMod + rangeMod + sizeMod + moveAndAimMod; return(new Tuple <float, float>(total, expectedDamage)); }
private void AddEquipRangedWeaponActionToBag(BattleSoldier soldier) { int handsFree = soldier.HandsFree; // we're standing here without a readied ranged weapon; we should do something about that if (soldier.RangedWeapons.Count == 1 && handsFree >= 1) { // the easiest case... ready our one ranged weapon soldier.CurrentSpeed = 0; _shootActionBag.Add(new ReadyRangedWeaponAction(soldier, soldier.RangedWeapons[0])); } else if (soldier.RangedWeapons.Count > 1 && handsFree >= 1) { // ugh, this is a decision with a lot of factors that will only come up rarely // for now, let's go with the longer ranged weapon soldier.CurrentSpeed = 0; _shootActionBag.Add(new ReadyRangedWeaponAction(soldier, soldier.RangedWeapons.OrderByDescending(w => w.Template.MaximumRange).First())); } }
private Tuple <float, float, RangedWeapon> ShouldShootAtRange(BattleSoldier soldier, BattleSoldier target, float range, bool useBulk) { RangedWeapon bestWeapon = null; float bestAccuracy = 0; float bestDamage = -0; foreach (RangedWeapon weapon in soldier.EquippedRangedWeapons.OrderByDescending(w => w.Template.DamageMultiplier)) { Tuple <float, float> hitAndDamage = EstimateHitAndDamage(soldier, target, weapon, range, useBulk ? -2 : 0); // if not likely to break through armor, there's little point if (hitAndDamage.Item1 > 6.66f && hitAndDamage.Item2 > bestDamage) { // about a 1/10 chance of hitting bestAccuracy = hitAndDamage.Item1; bestDamage = hitAndDamage.Item2; bestWeapon = weapon; } } return(new Tuple <float, float, RangedWeapon>(bestAccuracy, bestDamage, bestWeapon)); }
private void AddMeleeActionsToBag(BattleSoldier soldier) { // for now just attack, don't worry about cooler moves // if we have melee weapons but none are equipped, we should change that if (soldier.EquippedMeleeWeapons.Count == 0 && soldier.MeleeWeapons.Count > 0) { soldier.CurrentSpeed = 0; _shootActionBag.Add(new ReadyMeleeWeaponAction(soldier, soldier.MeleeWeapons[0])); } else { float distance = _grid.GetNearestEnemy(soldier.Soldier.Id, out int closestEnemyId); if (distance != 1) { throw new InvalidOperationException("Attempting to melee with no adjacent enemy"); } BattleSoldier enemy = _opposingSoldierIdSquadMap[closestEnemyId].Soldiers.Single(s => s.Soldier.Id == closestEnemyId); soldier.CurrentSpeed = 0; MeleeWeapon weapon = soldier.EquippedMeleeWeapons.Count == 0 ? _defaultMeleeWeapon : soldier.EquippedMeleeWeapons[0]; _meleeActionBag.Add(new MeleeAttackAction(soldier, enemy, weapon, false, _woundResolutionBag, _log)); } }
private float CalculateOptimalDistance(BattleSoldier soldier, float targetSize, float targetArmor, float targetCon) { int freeHands = soldier.Soldier.FunctioningHands; if (freeHands == 0) { // with no hands free, there's not much combat left for this soldier return(-1); } float range = 0; var weapons = soldier.EquippedRangedWeapons.OrderByDescending(w => w.Template.MaximumRange); foreach (RangedWeapon weapon in weapons) { float hitRange = EstimateHitDistance(soldier.Soldier, weapon, targetSize, freeHands); float damRange = EstimateKillDistance(weapon, targetArmor, targetCon); float minVal = Mathf.Min(hitRange, damRange); if (minVal > range) { range = minVal; } } return(range); }
private void AddAdvanceActionsToBag(BattleSoldier soldier) { // for now advance toward closest enemy; // down the road, we may want to advance toward a rearward enemy, ignoring the closest enemy Tuple <int, int> currentPosition = _grid.GetSoldierPosition(soldier.Soldier.Id); float distance = _grid.GetNearestEnemy(soldier.Soldier.Id, out int closestEnemyId); float moveSpeed = soldier.GetMoveSpeed(); if (distance < moveSpeed) { AddChargeActionsToBag(soldier); } else { Tuple <int, int> enemyPosition = _grid.GetSoldierPosition(closestEnemyId); Tuple <int, int> line = new Tuple <int, int>(enemyPosition.Item1 - currentPosition.Item1, enemyPosition.Item2 - currentPosition.Item2); // soldier can't get there in one move, advance as far as possible AddMoveAction(soldier, moveSpeed, line); // should the soldier shoot along the way? ShootIfReasonable(soldier, true); } }
public void RemoveSoldier(BattleSoldier soldier) { Soldiers.Remove(soldier); }
private void AddChargeActionsToBag(BattleSoldier soldier) { if (soldier.IsInMelee) { // determine what sort of manuver to make AddMeleeActionsToBag(soldier); } else { // get stuck in // move adjacent to nearest enemy // TODO: handle when someone else in the same squad wants to use the same spot // TODO: probably by letting the one with the lower id have it, and the higher id has to Tuple <int, int> currentPosition = _grid.GetSoldierPosition(soldier.Soldier.Id); float distance = _grid.GetNearestEnemy(soldier.Soldier.Id, out int closestEnemyId); float moveSpeed = soldier.GetMoveSpeed(); Tuple <int, int> enemyPosition = _grid.GetSoldierPosition(closestEnemyId); if (distance > moveSpeed + 1) { Tuple <int, int> moveVector = new Tuple <int, int>(enemyPosition.Item1 - currentPosition.Item1, enemyPosition.Item2 - currentPosition.Item2); // we can't make it to an enemy in one move // soldier can't get there in one move, advance as far as possible AddMoveAction(soldier, moveSpeed, moveVector); // should the soldier shoot along the way? ShootIfReasonable(soldier, true); } else { Tuple <int, int> newPos = _grid.GetClosestOpenAdjacency(currentPosition, enemyPosition); BattleSquad oppSquad = _opposingSoldierIdSquadMap[closestEnemyId]; if (newPos == null) { // find the next closest // okay, this is one of those times where I made something because it made me feel smart, // but it's probably unreadable so I should change it later // basically, foreach soldier in the squad of the closest enemy, except the closest enemy (who we already checked) // get their locations, and then sort it according to distance square // PROTIP: SQRT is a relatively expensive operation, so sort by distance squares when it's about comparative, not absolute, distance var map = oppSquad.Soldiers .Where(s => s.Soldier.Id != closestEnemyId) .Select(s => new Tuple <int, Tuple <int, int> >(s.Soldier.Id, _grid.GetSoldierPosition(s.Soldier.Id))) .Select(t => new Tuple <int, Tuple <int, int>, Tuple <int, int> >(t.Item1, t.Item2, new Tuple <int, int>(t.Item2.Item1 - currentPosition.Item1, t.Item2.Item2 - currentPosition.Item2))) .Select(u => new Tuple <int, Tuple <int, int>, int>(u.Item1, u.Item2, (u.Item3.Item1 * u.Item3.Item1 + u.Item3.Item2 * u.Item3.Item2))) .OrderBy(u => u.Item3); foreach (Tuple <int, Tuple <int, int>, int> soldierData in map) { newPos = _grid.GetClosestOpenAdjacency(currentPosition, soldierData.Item2); if (newPos != null) { AddChargeActionsHelper(soldier, soldierData.Item1, currentPosition, Mathf.Sqrt(soldierData.Item3), oppSquad, newPos); break; } } if (newPos == null) { // we weren't able to find an enemy to get near, guess we try to find someone to shoot, instead? Debug.Log("ISoldier in squad engaged in melee couldn't find anyone to attack"); AddStandingActionsToBag(soldier); } } else { AddChargeActionsHelper(soldier, closestEnemyId, currentPosition, distance, oppSquad, newPos); } } } }