Example #1
0
        public void IntVector2AddLinearGradientEightWay()
        {
            var map = new HeatMap();

            map.AddLinearGradientEightWay(new IntVector2(0, 0), 10, 10, -1);
            map.AddLinearGradientEightWay(new IntVector2(1, 1), 2, 2, -1);
            Assert.AreEqual(11, map[0, 0]);
            Assert.AreEqual(11, map[1, 1]);
            Assert.AreEqual(0, map[99, 99]);
        }
Example #2
0
        /// <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);
                }