/// <summary> /// Resolves the battle. /// </summary> public void Resolve() { // update memories foreach (var sobj in StarSystem.SpaceObjects.Where(x => !x.IsMemory).ToArray()) { sobj.UpdateEmpireMemories(); } Current.Add(this); var reloads = new SafeDictionary <Component, double>(); var locations = new SafeDictionary <ICombatant, IntVector2>(); PlaceCombatants(locations); Events = new List <IList <IBattleEvent> >(); UpdateBounds(0, locations.Values); // let all combatants scan each other foreach (var c in Combatants) { c.UpdateEmpireMemories(); } // make a query so we can check who's alive var alives = Combatants.Where(q => q.IsAlive); for (int i = 0; i < MaxRounds; i++) { var combatSpeeds = new SafeDictionary <ICombatant, double>(); var multiplex = new SafeDictionary <ICombatant, HashSet <ICombatant> >(true); foreach (var c in Combatants) { combatSpeeds[c] = c.CombatSpeed; } int GetCombatSpeedThisRound(ICombatant c) { return((int)(combatSpeeds[c] + CombatSpeedBuffer[c])); } Events.Add(new List <IBattleEvent>()); if (i == 0) { // first round, all combatants appear foreach (var c in Combatants) { Events.Last().Add(new CombatantAppearsEvent(this, c, locations[c])); } } var turnorder = alives.OrderBy(x => x is Seeker ? 1 : 0).ThenBy(x => combatSpeeds[x]).ThenShuffle(Dice).ToArray(); // phase 0: reload weapons foreach (var w in turnorder.SelectMany(q => q.Weapons)) { reloads[w]--; if (reloads[w] < 0) { reloads[w] = 0; } } // phase 1: combatants move starting with the slowest (so the faster ships get to react to their moves) - but seekers go last so they get a chance to hit foreach (var c in turnorder) { var oldpos = locations[c]; if (c is Seeker s) { if (locations[s] == null) { continue; // HACK - seeker is destroyed but still showing up in turn order } if (locations[s.Target] == null) { s.Hitpoints = 0; // seekers self destruct when their target is destroyed Events.Last().Add(new CombatantDestroyedEvent(this, s, locations[s])); continue; } s.DistanceTraveled += Math.Min(GetCombatSpeedThisRound(c), locations[s].DistanceToEightWay(locations[s.Target])); locations[s] = IntVector2.InterpolateEightWay(locations[s], locations[s.Target], GetCombatSpeedThisRound(c)); if (s.DistanceTraveled > s.WeaponInfo.MaxRange) { s.Hitpoints = 0; Events.Last().Add(new CombatantDestroyedEvent(this, s, locations[s])); } } else { // TODO - both pursue target and evade scary enemies at the same time using heatmap // find out how good each target is var targetiness = new SafeDictionary <ICombatant, double>(); foreach (var target in alives.Where(x => c.IsHostileTo(x.Owner) && (c.CanTarget(x) || (x is Planet && c is ICargoContainer cc && cc.Cargo.Units.OfType <Troop>().Any())))) { targetiness[target] = 1d / (locations[target] - locations[c]).LengthEightWay; } if (!targetiness.Any()) { // evade enemies var heatmap = new HeatMap(); foreach (var e in alives.Where(x => x.IsHostileTo(c.Owner) && x.CanTarget(c))) { int threat; if (e.Weapons.Any()) { threat = GetCombatSpeedThisRound(e) + e.Weapons.Where(w => w.CanTarget(c)).Max(w => w.Template.WeaponMaxRange); } else { threat = 0; } heatmap.AddLinearGradientEightWay(locations[e], threat, threat, -1); } if (c.FillsCombatTile) { // only one ship/base/planet per tile foreach (var tile in heatmap.ToArray()) { if (locations.Any(q => q.Key.FillsCombatTile && q.Value == tile.Key)) { heatmap.Remove(tile.Key); } } } if (heatmap.Any()) { locations[c] = heatmap.FindMin(locations[c], GetCombatSpeedThisRound(c)); } } else { // move to max range that we can inflict max damage on best target var goodTargets = targetiness.Where(x => !IgnoredTargets[c].Contains(x.Key)).WithMax(x => x.Value); ICombatant bestTarget = null; if (goodTargets.Any()) { bestTarget = goodTargets.First().Key; } if (bestTarget == null) { // try previously ignored targets IgnoredTargets[c].Clear(); goodTargets = targetiness.Where(x => !IgnoredTargets[c].Contains(x.Key)).WithMax(x => x.Value); if (goodTargets.Any()) { bestTarget = goodTargets.First().Key; } } if (bestTarget != null) { gotosAreEvil: var maxdmg = 0; var maxdmgrange = 0; if (c.Weapons.Any()) { for (var range = 0; range < c.Weapons.Max(w => w.Template.WeaponMaxRange); range++) { var dmg = c.Weapons.Where(w => w.CanTarget(bestTarget)).Sum(w => w.Template.GetWeaponDamage(range)); if (dmg >= maxdmg) { maxdmg = dmg; maxdmgrange = range; } } } if (c.Weapons.Any(w => w.Template.ComponentTemplate.WeaponInfo.IsSeeker) && locations[c].DistanceToEightWay(locations[bestTarget]) > DistancesToTargets[c]) { // adjust desired range due to seeker speed and target speed if retreating var roundsToClose = c.Weapons.Where(w => w.Template.ComponentTemplate.WeaponInfo.IsSeeker).Max(w => (int)Math.Ceiling((double)w.Template.WeaponMaxRange / (double)(w.Template.ComponentTemplate.WeaponInfo as SeekingWeaponInfo).SeekerSpeed)); var distanceAdjustment = (int)Ceiling(combatSpeeds[bestTarget] * roundsToClose); maxdmgrange -= distanceAdjustment; if (maxdmgrange < 0) { maxdmgrange = 0; } } var targetPos = locations[bestTarget]; var tiles = new HashSet <IntVector2>(); for (var x = targetPos.X - maxdmgrange; x <= targetPos.X + maxdmgrange; x++) { tiles.Add(new IntVector2(x, targetPos.Y - maxdmgrange)); tiles.Add(new IntVector2(x, targetPos.Y + maxdmgrange)); } for (var y = targetPos.Y - maxdmgrange; y <= targetPos.Y + maxdmgrange; y++) { tiles.Add(new IntVector2(targetPos.X - maxdmgrange, y)); tiles.Add(new IntVector2(targetPos.X + maxdmgrange, y)); } if (c.FillsCombatTile) { foreach (var tile in tiles.ToArray()) { if (locations.Any(q => q.Key.FillsCombatTile && q.Value == tile)) { tiles.Remove(tile); } } } if (tiles.Any()) { var closest = tiles.WithMin(t => t.DistanceToEightWay(locations[c])).First(); locations[c] = IntVector2.InterpolateEightWay(locations[c], closest, GetCombatSpeedThisRound(c), vec => locations.Values.Contains(vec)); var newdist = locations[c].DistanceToEightWay(locations[bestTarget]); if (DistancesToTargets.ContainsKey(c) && newdist >= DistancesToTargets[c] && combatSpeeds[c] <= combatSpeeds[bestTarget] && !c.Weapons.Any(w => w.Template.WeaponMaxRange >= newdist)) { DistancesToTargets.Remove(c); IgnoredTargets[c].Add(bestTarget); // can't catch it, might as well find a new target goodTargets = targetiness.Where(x => !IgnoredTargets[c].Contains(x.Key)).WithMax(x => x.Value); bestTarget = null; if (goodTargets.Any()) { bestTarget = goodTargets.First().Key; } if (bestTarget == null) { goto gotosAreVeryEvil; } goto gotosAreEvil; } else { DistancesToTargets[c] = newdist; } } } else { DistancesToTargets.Remove(c); } } } gotosAreVeryEvil: if (locations[c] != oldpos) { Events.Last().Add(new CombatantMovesEvent(this, c, oldpos, locations[c])); } } UpdateBounds(i, locations.Values); // phase 2: combatants launch units foreach (var c in turnorder) { // find launchable units var unitsToLaunch = new List <(ICombatant Launcher, SpaceVehicle Launchee)>(); if (c is Planet) { // planets can launch infinite units per turn var p = (Planet)c; if (p.Cargo != null && p.Cargo.Units != null) { foreach (var u in p.Cargo.Units.OfType <SpaceVehicle>()) { unitsToLaunch.Add((p, u)); } } } else if (c is ICargoTransferrer) { // ships, etc. can launch units based on abilities var ct = (ICargoTransferrer)c; foreach (var vt in Enum.GetValues(typeof(VehicleTypes)).Cast <VehicleTypes>().Distinct()) { var rate = ct.GetAbilityValue("Launch/Recover " + vt.ToSpacedString() + "s").ToInt(); foreach (var u in ct.Cargo.Units.Where(u => u.Design.VehicleType == vt).OfType <SpaceVehicle>().Take(rate)) { unitsToLaunch.Add((c, u)); } } } // launch them temporarily for combat foreach (var info in unitsToLaunch) { Launchers[info.Launchee] = info.Launcher; if (info.Launcher is ICargoTransferrer ct && info.Launchee is IUnit u) { ct.RemoveUnit(u); } Combatants.Add(info.Item2); StartCombatants[info.Item2.ID] = info.Item2.Copy(); for (var ix = 0; ix < info.Item2.Weapons.Count(); ix++) { var w = info.Item2.Weapons.ElementAt(ix); var wc = StartCombatants[info.Item2.ID].Weapons.ElementAt(ix); } locations[info.Launchee] = new IntVector2(locations[info.Launcher]); Events.Last().Add(new CombatantLaunchedEvent(this, info.Launcher, info.Launchee, locations[info.Launchee])); } } turnorder = alives.OrderBy(x => x.CombatSpeed).ThenShuffle(Dice).ToArray(); // phase 3: combatants fire point defense non-warhead weapons starting with the fastest (so the faster ships get to inflict damage first and possibly KO enemies preventing them from firing back) foreach (var c in turnorder.Reverse()) { foreach (var w in c.Weapons.Where(w => w.Template.ComponentTemplate.WeaponInfo.IsPointDefense && !w.Template.ComponentTemplate.WeaponInfo.IsWarhead)) { TryFireWeapon(c, w, reloads, locations, multiplex); } } turnorder = alives.OrderBy(x => x.CombatSpeed).ThenShuffle(Dice).ToArray(); // phase 4: point defense seekers detonate foreach (var s in turnorder.Reverse().OfType <Seeker>().Where(s => s.WeaponInfo.IsPointDefense)) { CheckSeekerDetonation(s, locations); } turnorder = alives.OrderBy(x => x.CombatSpeed).ThenShuffle(Dice).ToArray(); // phase 5: ships fire non-PD non-warhead weapons starting with the fastest (so the faster ships get to inflict damage first and possibly KO enemies preventing them from firing back) foreach (var c in turnorder.Reverse()) { foreach (var w in c.Weapons.Where(w => !w.Template.ComponentTemplate.WeaponInfo.IsPointDefense && !w.Template.ComponentTemplate.WeaponInfo.IsWarhead)) { TryFireWeapon(c, w, reloads, locations, multiplex); } } turnorder = alives.OrderBy(x => x.CombatSpeed).ThenShuffle(Dice).ToArray(); // phase 6: non-PD seekers detonate foreach (var s in turnorder.Reverse().OfType <Seeker>().Where(s => !s.WeaponInfo.IsPointDefense)) { CheckSeekerDetonation(s, locations); } turnorder = alives.OrderBy(x => x.CombatSpeed).ThenShuffle(Dice).ToArray(); // phase 7: ramming! only activates if ship has no other weapons foreach (var c in turnorder.Reverse()) { if (!c.Weapons.Any(w => !w.Template.ComponentTemplate.WeaponInfo.IsWarhead)) { // TODO - add damage from ship HP on both sides foreach (var w in c.Weapons.Where(w => w.Template.ComponentTemplate.WeaponInfo.IsWarhead)) { TryFireWeapon(c, w, reloads, locations, multiplex); } } } turnorder = alives.OrderBy(x => x.CombatSpeed).ThenShuffle(Dice).ToArray(); // TODO - boarding // phase 8: drop troops foreach (var c in turnorder.Reverse()) { if (c is ICargoTransferrer cc && cc.AllUnits.OfType <Troop>().Any()) { // find enemy planets in the same square var dropTargets = locations.Where(q => q.Key != c && q.Value == locations[c] && q.Key is Planet p && c.IsHostileTo(p.Owner)).Select(q => q.Key).Cast <Planet>(); var dropTarget = dropTargets.PickRandom(Dice); if (dropTarget != null) { var cd = new CargoDelta(); cd.UnitTypeTonnage.Add(VehicleTypes.Troop, null); cc.TransferCargo(cd, dropTarget, cc.Owner, true); var groundBattle = new GroundBattle(dropTarget); groundBattle.Resolve(); } } } // clear used combat speed buffer speed foreach (var x in Combatants) { CombatSpeedBuffer[x] += x.CombatSpeed - Floor(x.CombatSpeed); CombatSpeedBuffer[x] -= Floor(CombatSpeedBuffer[x]); } UpdateBounds(i, locations.Values); bool hostile = false; foreach (var a in alives) { foreach (var b in alives) { // TODO - check if ships want to ram even if they have no weapons if (a.IsHostileTo(b.Owner) && a.Weapons.Any()) { hostile = true; break; } } if (hostile) { break; } } if (!hostile) { break; } } // recover units var orphans = new List <IUnit>(); foreach (var u in Combatants.OfType <IUnit>()) { if (Launchers[u] is ICargoTransferrer cc && cc.CargoStorageFree() >= u.Design.Hull.Size && u.Owner == cc.Owner) { cc.Cargo.Units.Add(u); }