Purchase reserves for CAP/CAS.

Next turn's defenses should be planned in preference to expanding
offensive capabilities.

Fixes https://github.com/Khopa/dcs_liberation/issues/511
This commit is contained in:
Dan Albert 2020-12-25 19:15:31 -08:00
parent c7f9bfbb43
commit a43b100781
2 changed files with 47 additions and 15 deletions

View File

@ -9,6 +9,7 @@ Saves from 2.3 are not compatible with 2.4.
* **[Campaign AI]** Auto-purchase now prefers airfields that are not within range of the enemy.
* **[Campaign AI]** Auto-purchase now prefers the best aircraft for the task, but will attempt to maintain some variety.
* **[Campaign AI]** Opfor now sells off odd aircraft since they're unlikely to be used.
* **[Campaign AI]** Reserve aircraft will be ordered if needed to prioritize next turn's CAP/CAS over offensive missions.
* **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior.
* **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany.

View File

@ -469,8 +469,16 @@ class CoalitionMissionPlanner:
self.threat_zones = self.game.threat_zone_for(not self.is_player)
self.procurement_requests: List[AircraftProcurementRequest] = []
def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order."""
def critical_missions(self) -> Iterator[ProposedMission]:
"""Identifies the most important missions to plan this turn.
Non-critical missions that cannot be fulfilled will create purchase
orders for the next turn. Critical missions will create a purchase order
unless the mission can be doubly fulfilled. In other words, the AI will
attempt to have *double* the aircraft it needs for these missions to
ensure that they can be planned again next turn even if all aircraft are
eliminated this turn.
"""
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points():
yield ProposedMission(cp, [
@ -485,6 +493,10 @@ class CoalitionMissionPlanner:
EscortType.AirToAir),
])
def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order."""
yield from self.critical_missions()
# Find enemy SAM sites with ranges that cover friendly CPs, front lines,
# or objects, plan DEAD.
# Find enemy SAM sites with ranges that extend to within 50 nmi of
@ -542,6 +554,9 @@ class CoalitionMissionPlanner:
for proposed_mission in self.propose_missions():
self.plan_mission(proposed_mission)
for critical_mission in self.critical_missions():
self.plan_mission(critical_mission, reserves=True)
self.stagger_missions()
for cp in self.objective_finder.friendly_control_points():
@ -551,32 +566,40 @@ class CoalitionMissionPlanner:
f"{available} {aircraft.id} from {cp}")
def plan_flight(self, mission: ProposedMission, flight: ProposedFlight,
builder: PackageBuilder,
missing_types: Set[FlightType]) -> None:
builder: PackageBuilder, missing_types: Set[FlightType],
for_reserves: bool) -> None:
if not builder.plan_flight(flight):
missing_types.add(flight.task)
self.procurement_requests.append(AircraftProcurementRequest(
purchase_order = AircraftProcurementRequest(
near=mission.location,
range=flight.max_distance,
task_capability=flight.task,
number=flight.num_aircraft
))
)
if for_reserves:
# Reserves are planned for critical missions, so prioritize
# those orders over aircraft needed for non-critical missions.
self.procurement_requests.insert(0, purchase_order)
else:
self.procurement_requests.append(purchase_order)
def scrub_mission_missing_aircraft(
self, mission: ProposedMission, builder: PackageBuilder,
missing_types: Set[FlightType],
not_attempted: Iterable[ProposedFlight]) -> None:
not_attempted: Iterable[ProposedFlight],
reserves: bool) -> None:
# Try to plan the rest of the mission just so we can count the missing
# types to buy.
for flight in not_attempted:
self.plan_flight(mission, flight, builder, missing_types)
self.plan_flight(mission, flight, builder, missing_types, reserves)
missing_types_str = ", ".join(
sorted([t.name for t in missing_types]))
builder.release_planned_aircraft()
desc = "reserve aircraft" if reserves else "aircraft"
self.message(
"Insufficient aircraft",
f"Not enough aircraft in range for {mission.location.name} "
f"Not enough {desc} in range for {mission.location.name} "
f"capable of: {missing_types_str}")
def check_needed_escorts(
@ -589,7 +612,8 @@ class CoalitionMissionPlanner:
threats[EscortType.Sead] = True
return threats
def plan_mission(self, mission: ProposedMission) -> None:
def plan_mission(self, mission: ProposedMission,
reserves: bool = False) -> None:
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
if self.game.settings.perf_ai_parking_start:
@ -616,11 +640,12 @@ class CoalitionMissionPlanner:
# If the package does not need escorts they may be pruned.
escorts.append(proposed_flight)
continue
self.plan_flight(mission, proposed_flight, builder, missing_types)
self.plan_flight(mission, proposed_flight, builder, missing_types,
reserves)
if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
escorts)
escorts, reserves)
return
# Create flight plans for the main flights of the package so we can
@ -640,14 +665,20 @@ class CoalitionMissionPlanner:
# impossible.
assert escort.escort_type is not None
if needed_escorts[escort.escort_type]:
self.plan_flight(mission, escort, builder,
missing_types)
self.plan_flight(mission, escort, builder, missing_types,
reserves)
# Check again for unavailable aircraft. If the escort was required and
# none were found, scrub the mission.
if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
escorts)
escorts, reserves)
return
if reserves:
# Mission is planned reserves which will not be used this turn.
# Return reserves to the inventory.
builder.release_planned_aircraft()
return
package = builder.build()