diff --git a/changelog.md b/changelog.md index 607fb848..8eb68aa0 100644 --- a/changelog.md +++ b/changelog.md @@ -17,6 +17,7 @@ * **[UI]** Allow changing waypoint names in FlightEdit's waypoints tab * **[Waypoints]** Allow user to add navigation waypoints where possible without degrading to a custom flight-plan * **[Campaign Management]** Improve squadron retreat logic to account for parking-slot sizes +* **[Autoplanner]** Support for auto-planning Air Assaults ## Fixes * **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index bb9aca1c..f88410d0 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -167,6 +167,24 @@ class ObjectiveFinder: yield cp break + def vulnerable_enemy_control_points(self) -> Iterator[ControlPoint]: + """Iterates over enemy CPs that are vulnerable to Air Assault. + Vulnerability is defined as any unit being alive in the CP's "blocking_capture" groups. + """ + for cp in self.enemy_control_points(): + include = True + for tgo in cp.connected_objectives: + if tgo.distance_to(cp) > cp.CAPTURE_DISTANCE.meters: + continue + for u in tgo.units: + if u.is_vehicle and u.alive: + include = False + break + if not include: + break + if include: + yield cp + def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]: parking_type = ParkingType() parking_type.include_rotary_wing = True diff --git a/game/commander/tasks/compound/capturebase.py b/game/commander/tasks/compound/capturebase.py index 378cb13e..1c2d102b 100644 --- a/game/commander/tasks/compound/capturebase.py +++ b/game/commander/tasks/compound/capturebase.py @@ -7,6 +7,7 @@ from game.commander.tasks.compound.destroyenemygroundunits import ( from game.commander.tasks.compound.reduceenemyfrontlinecapacity import ( ReduceEnemyFrontLineCapacity, ) +from game.commander.tasks.primitive.airassault import PlanAirAssault from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack from game.commander.theaterstate import TheaterState from game.htn import CompoundTask, Method @@ -22,6 +23,7 @@ class CaptureBase(CompoundTask[TheaterState]): yield [DestroyEnemyGroundUnits(self.front_line)] if self.worth_destroying_ammo_depots(state): yield [ReduceEnemyFrontLineCapacity(self.enemy_cp(state))] + yield [PlanAirAssault(self.enemy_cp(state))] def enemy_cp(self, state: TheaterState) -> ControlPoint: return self.front_line.control_point_hostile_to(state.context.coalition.player) diff --git a/game/commander/tasks/primitive/airassault.py b/game/commander/tasks/primitive/airassault.py new file mode 100644 index 00000000..2130b7e2 --- /dev/null +++ b/game/commander/tasks/primitive/airassault.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from game.ato.flighttype import FlightType +from game.commander.tasks.packageplanningtask import PackagePlanningTask +from game.commander.theaterstate import TheaterState +from game.theater import ControlPoint + + +@dataclass +class PlanAirAssault(PackagePlanningTask[ControlPoint]): + def preconditions_met(self, state: TheaterState) -> bool: + if self.target not in state.vulnerable_control_points: + return False + if not self.target_area_preconditions_met(state): + return False + return super().preconditions_met(state) + + def apply_effects(self, state: TheaterState) -> None: + state.vulnerable_control_points.remove(self.target) + + def propose_flights(self) -> None: + size = self.get_flight_size() + self.propose_flight(FlightType.AIR_ASSAULT, size) + self.propose_common_escorts() diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py index ca60c311..2c64b66a 100644 --- a/game/commander/theaterstate.py +++ b/game/commander/theaterstate.py @@ -60,6 +60,7 @@ class TheaterState(WorldState["TheaterState"]): strike_targets: list[TheaterGroundObject] enemy_barcaps: list[ControlPoint] threat_zones: ThreatZones + vulnerable_control_points: list[ControlPoint] def _rebuild_threat_zones(self) -> None: """Recreates the theater's threat zones based on the current planned state.""" @@ -133,6 +134,7 @@ class TheaterState(WorldState["TheaterState"]): # IADS threats so that DegradeIads will consider it a threat later. threatening_air_defenses=self.threatening_air_defenses, detecting_air_defenses=self.detecting_air_defenses, + vulnerable_control_points=self.vulnerable_control_points, ) @classmethod @@ -181,4 +183,5 @@ class TheaterState(WorldState["TheaterState"]): strike_targets=list(finder.strike_targets()), enemy_barcaps=list(game.theater.control_points_for(not player)), threat_zones=game.threat_zone_for(not player), + vulnerable_control_points=list(finder.vulnerable_enemy_control_points()), )