mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Plan multiple CAP rounds per turn.
On station time for CAP is only 30 minutes, so plan three cycles to give ~90 minutes of CAP coverage. Default starting budget has increased significantly to account for the greatly increased aircraft needs on turn 1. Fixes https://github.com/Khopa/dcs_liberation/issues/673
This commit is contained in:
parent
d3b1f6110f
commit
3a9f585b6b
@ -10,6 +10,7 @@ Saves from 2.3 are not compatible with 2.4.
|
|||||||
* **[Campaign AI]** Auto-purchase now prefers the best aircraft for the task, but will attempt to maintain some variety.
|
* **[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]** 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.
|
* **[Campaign AI]** Reserve aircraft will be ordered if needed to prioritize next turn's CAP/CAS over offensive missions.
|
||||||
|
* **[Campaign AI]** Multiple rounds of CAP will be planned (roughly 90 minutes of coverage). Default starting budget has increased to account for the increased need for aircraft.
|
||||||
* **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior.
|
* **[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.
|
* **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany.
|
||||||
* **[Balance]** Opfor now gains income using the same rules as the player, significantly increasing their income relative to the player for most campaigns.
|
* **[Balance]** Opfor now gains income using the same rules as the player, significantly increasing their income relative to the player for most campaigns.
|
||||||
|
|||||||
10
game/game.py
10
game/game.py
@ -318,13 +318,18 @@ class Game:
|
|||||||
|
|
||||||
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
|
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
|
||||||
red_planner: CoalitionMissionPlanner) -> None:
|
red_planner: CoalitionMissionPlanner) -> None:
|
||||||
|
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
|
||||||
|
# gets much more of the budget that turn. Otherwise budget (after
|
||||||
|
# repairs) is split evenly between air and ground.
|
||||||
|
ground_portion = 0.1 if self.turn == 0 else 0.5
|
||||||
self.budget = ProcurementAi(
|
self.budget = ProcurementAi(
|
||||||
self,
|
self,
|
||||||
for_player=True,
|
for_player=True,
|
||||||
faction=self.player_faction,
|
faction=self.player_faction,
|
||||||
manage_runways=self.settings.automate_runway_repair,
|
manage_runways=self.settings.automate_runway_repair,
|
||||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||||
manage_aircraft=self.settings.automate_aircraft_reinforcements
|
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||||
|
front_line_budget_share=ground_portion
|
||||||
).spend_budget(self.budget, blue_planner.procurement_requests)
|
).spend_budget(self.budget, blue_planner.procurement_requests)
|
||||||
|
|
||||||
self.enemy_budget = ProcurementAi(
|
self.enemy_budget = ProcurementAi(
|
||||||
@ -333,7 +338,8 @@ class Game:
|
|||||||
faction=self.enemy_faction,
|
faction=self.enemy_faction,
|
||||||
manage_runways=True,
|
manage_runways=True,
|
||||||
manage_front_line=True,
|
manage_front_line=True,
|
||||||
manage_aircraft=True
|
manage_aircraft=True,
|
||||||
|
front_line_budget_share=ground_portion
|
||||||
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
||||||
|
|
||||||
def message(self, text: str) -> None:
|
def message(self, text: str) -> None:
|
||||||
|
|||||||
@ -38,13 +38,17 @@ class AircraftProcurementRequest:
|
|||||||
class ProcurementAi:
|
class ProcurementAi:
|
||||||
def __init__(self, game: Game, for_player: bool, faction: Faction,
|
def __init__(self, game: Game, for_player: bool, faction: Faction,
|
||||||
manage_runways: bool, manage_front_line: bool,
|
manage_runways: bool, manage_front_line: bool,
|
||||||
manage_aircraft: bool) -> None:
|
manage_aircraft: bool, front_line_budget_share: float) -> None:
|
||||||
|
if front_line_budget_share > 1.0:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
self.game = game
|
self.game = game
|
||||||
self.is_player = for_player
|
self.is_player = for_player
|
||||||
self.faction = faction
|
self.faction = faction
|
||||||
self.manage_runways = manage_runways
|
self.manage_runways = manage_runways
|
||||||
self.manage_front_line = manage_front_line
|
self.manage_front_line = manage_front_line
|
||||||
self.manage_aircraft = manage_aircraft
|
self.manage_aircraft = manage_aircraft
|
||||||
|
self.front_line_budget_share = front_line_budget_share
|
||||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
|
|
||||||
def spend_budget(
|
def spend_budget(
|
||||||
@ -53,7 +57,7 @@ class ProcurementAi:
|
|||||||
if self.manage_runways:
|
if self.manage_runways:
|
||||||
budget = self.repair_runways(budget)
|
budget = self.repair_runways(budget)
|
||||||
if self.manage_front_line:
|
if self.manage_front_line:
|
||||||
armor_budget = math.ceil(budget / 2)
|
armor_budget = math.ceil(budget * self.front_line_budget_share)
|
||||||
budget -= armor_budget
|
budget -= armor_budget
|
||||||
budget += self.reinforce_front_line(armor_budget)
|
budget += self.reinforce_front_line(armor_budget)
|
||||||
|
|
||||||
|
|||||||
@ -118,6 +118,15 @@ class Package:
|
|||||||
return max(times)
|
return max(times)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mission_departure_time(self) -> Optional[timedelta]:
|
||||||
|
times = []
|
||||||
|
for flight in self.flights:
|
||||||
|
times.append(flight.flight_plan.mission_departure_time)
|
||||||
|
if times:
|
||||||
|
return max(times)
|
||||||
|
return None
|
||||||
|
|
||||||
def add_flight(self, flight: Flight) -> None:
|
def add_flight(self, flight: Flight) -> None:
|
||||||
"""Adds a flight to the package."""
|
"""Adds a flight to the package."""
|
||||||
self.flights.append(flight)
|
self.flights.append(flight)
|
||||||
|
|||||||
@ -481,6 +481,14 @@ class CoalitionMissionPlanner:
|
|||||||
"""
|
"""
|
||||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||||
for cp in self.objective_finder.vulnerable_control_points():
|
for cp in self.objective_finder.vulnerable_control_points():
|
||||||
|
# Plan three rounds of CAP to give ~90 minutes coverage. Spacing
|
||||||
|
# these out appropriately is done in stagger_missions.
|
||||||
|
yield ProposedMission(cp, [
|
||||||
|
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||||
|
])
|
||||||
|
yield ProposedMission(cp, [
|
||||||
|
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||||
|
])
|
||||||
yield ProposedMission(cp, [
|
yield ProposedMission(cp, [
|
||||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||||
])
|
])
|
||||||
@ -698,10 +706,12 @@ class CoalitionMissionPlanner:
|
|||||||
|
|
||||||
dca_types = {
|
dca_types = {
|
||||||
FlightType.BARCAP,
|
FlightType.BARCAP,
|
||||||
FlightType.INTERCEPTION,
|
|
||||||
FlightType.TARCAP,
|
FlightType.TARCAP,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(
|
||||||
|
timedelta
|
||||||
|
)
|
||||||
non_dca_packages = [p for p in self.ato.packages if
|
non_dca_packages = [p for p in self.ato.packages if
|
||||||
p.primary_task not in dca_types]
|
p.primary_task not in dca_types]
|
||||||
|
|
||||||
@ -714,8 +724,22 @@ class CoalitionMissionPlanner:
|
|||||||
for package in self.ato.packages:
|
for package in self.ato.packages:
|
||||||
tot = TotEstimator(package).earliest_tot()
|
tot = TotEstimator(package).earliest_tot()
|
||||||
if package.primary_task in dca_types:
|
if package.primary_task in dca_types:
|
||||||
# All CAP missions should be on station ASAP.
|
previous_end_time = previous_cap_end_time[package.target]
|
||||||
package.time_over_target = tot
|
if tot > previous_end_time:
|
||||||
|
# Can't get there exactly on time, so get there ASAP. This
|
||||||
|
# will typically only happen for the first CAP at each
|
||||||
|
# target.
|
||||||
|
package.time_over_target = tot
|
||||||
|
else:
|
||||||
|
package.time_over_target = previous_end_time
|
||||||
|
|
||||||
|
departure_time = package.mission_departure_time
|
||||||
|
# Should be impossible for CAPs
|
||||||
|
if departure_time is None:
|
||||||
|
logging.error(
|
||||||
|
f"Could not determine mission end time for {package}")
|
||||||
|
continue
|
||||||
|
previous_cap_end_time[package.target] = departure_time
|
||||||
else:
|
else:
|
||||||
# But other packages should be spread out a bit. Note that take
|
# But other packages should be spread out a bit. Note that take
|
||||||
# times are delayed, but all aircraft will become active at
|
# times are delayed, but all aircraft will become active at
|
||||||
|
|||||||
@ -243,6 +243,11 @@ class FlightPlan:
|
|||||||
else:
|
else:
|
||||||
return timedelta(minutes=5)
|
return timedelta(minutes=5)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mission_departure_time(self) -> timedelta:
|
||||||
|
"""The time that the mission is complete and the flight RTBs."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class LoiterFlightPlan(FlightPlan):
|
class LoiterFlightPlan(FlightPlan):
|
||||||
@ -356,6 +361,10 @@ class FormationFlightPlan(LoiterFlightPlan):
|
|||||||
GroundSpeed.for_flight(self.flight, self.hold.alt)
|
GroundSpeed.for_flight(self.flight, self.hold.alt)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mission_departure_time(self) -> timedelta:
|
||||||
|
return self.split_time
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PatrollingFlightPlan(FlightPlan):
|
class PatrollingFlightPlan(FlightPlan):
|
||||||
@ -406,6 +415,10 @@ class PatrollingFlightPlan(FlightPlan):
|
|||||||
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
||||||
return self.patrol_start
|
return self.patrol_start
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mission_departure_time(self) -> timedelta:
|
||||||
|
return self.patrol_end_time
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BarCapFlightPlan(PatrollingFlightPlan):
|
class BarCapFlightPlan(PatrollingFlightPlan):
|
||||||
@ -678,6 +691,9 @@ class SweepFlightPlan(LoiterFlightPlan):
|
|||||||
GroundSpeed.for_flight(self.flight, self.hold.alt)
|
GroundSpeed.for_flight(self.flight, self.hold.alt)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def mission_departure_time(self) -> timedelta:
|
||||||
|
return self.sweep_end_time
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CustomFlightPlan(FlightPlan):
|
class CustomFlightPlan(FlightPlan):
|
||||||
@ -708,6 +724,10 @@ class CustomFlightPlan(FlightPlan):
|
|||||||
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mission_departure_time(self) -> timedelta:
|
||||||
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
|
||||||
class FlightPlanBuilder:
|
class FlightPlanBuilder:
|
||||||
"""Generates flight plans for flights."""
|
"""Generates flight plans for flights."""
|
||||||
|
|||||||
@ -30,7 +30,7 @@ jinja_env = Environment(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_BUDGET = 650
|
DEFAULT_BUDGET = 1600
|
||||||
|
|
||||||
|
|
||||||
class NewGameWizard(QtWidgets.QWizard):
|
class NewGameWizard(QtWidgets.QWizard):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user