diff --git a/.gitignore b/.gitignore index 9f22b322..5e953c36 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ a.py resources/tools/a.miz # User-specific stuff .idea/ +.env /kneeboards /liberation_preferences.json diff --git a/changelog.md b/changelog.md index 2f5e9d14..85053e9c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,16 @@ +# 2.5.1 + +## Features/Improvements + +* **[UI]** Engagement ranges are now displayed by default. +* **[UI]** Engagement range display generalized to work for all patrolling flight plans (BARCAP, TARCAP, and CAS). +* **[Flight Planner]** Front lines no longer project threat zones to avoid pushing BARCAPs back so much. TARCAPs will be forcibly planned but strike packages will not route around front lines even if it is reasonable to do so. + +## Fixes + +* **[Campaigns]** EWRs associated with a base will now only be generated near the base. +* **[Flight Planner]** Fixed error when generating AEW&C flight plans in campaigns with no front lines. + # 2.5.0 Saves from 2.4 are not compatible with 2.5. diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 80194189..16a8a0b0 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -365,7 +365,7 @@ class MizCampaignLoader: for group in self.ewrs: closest, distance = self.objective_info(group) if distance < self.BASE_DEFENSE_RADIUS: - closest.preset_locations.ewrs.append( + closest.preset_locations.base_ewrs.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) else: diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index f2a95200..4e4f3bbc 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -63,6 +63,7 @@ class LocationType(Enum): BaseAirDefense = "base air defense" Coastal = "coastal defense" Ewr = "EWR" + BaseEwr = "Base EWR" Garrison = "garrison" MissileSite = "missile site" OffshoreStrikeTarget = "offshore strike target" @@ -86,6 +87,9 @@ class PresetLocations: #: Locations used by EWRs. ewrs: List[PointWithHeading] = field(default_factory=list) + #: Locations used by Base EWRs. + base_ewrs: List[PointWithHeading] = field(default_factory=list) + #: Locations used by non-carrier ships. Carriers and LHAs are not random. ships: List[PointWithHeading] = field(default_factory=list) @@ -131,6 +135,8 @@ class PresetLocations: return self._random_from(self.coastal_defenses) if location_type == LocationType.Ewr: return self._random_from(self.ewrs) + if location_type == LocationType.BaseEwr: + return self._random_from(self.base_ewrs) if location_type == LocationType.Garrison: return self._random_from(self.base_garrisons) if location_type == LocationType.MissileSite: @@ -392,7 +398,7 @@ class ControlPoint(MissionTarget, ABC): for base_defense in self.base_defenses: p = PointWithHeading.from_point(base_defense.position, base_defense.heading) if isinstance(base_defense, EwrGroundObject): - self.preset_locations.ewrs.append(p) + self.preset_locations.base_ewrs.append(p) elif isinstance(base_defense, SamGroundObject): self.preset_locations.base_air_defense.append(p) elif isinstance(base_defense, VehicleGroupGroundObject): diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 5316b9c8..9ac71a56 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -455,7 +455,7 @@ class BaseDefenseGenerator: self.generate_base_defenses() def generate_ewr(self) -> None: - position = self.location_finder.location_for(LocationType.Ewr) + position = self.location_finder.location_for(LocationType.BaseEwr) if position is None: return diff --git a/game/threatzones.py b/game/threatzones.py index 85169f17..e4ad0c39 100644 --- a/game/threatzones.py +++ b/game/threatzones.py @@ -152,23 +152,6 @@ class ThreatZones: threat_zone = point.buffer(threat_range.meters) air_defenses.append(threat_zone) - for front_line in game.theater.conflicts(player): - vector = Conflict.frontline_vector( - front_line.control_point_a, front_line.control_point_b, game.theater - ) - - start = vector[0] - end = vector[0].point_from_heading(vector[1], vector[2]) - - line = LineString( - [ - ShapelyPoint(start.x, start.y), - ShapelyPoint(end.x, end.y), - ] - ) - doctrine = game.faction_for(player).doctrine - air_threats.append(line.buffer(doctrine.cap_engagement_range.meters)) - return cls( airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses) ) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 69453b37..0f79e8d9 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -450,7 +450,7 @@ class ObjectiveFinder: c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player) ) - def farthest_friendly_control_point(self) -> ControlPoint: + def farthest_friendly_control_point(self) -> Optional[ControlPoint]: """ Iterates over all friendly control points and find the one farthest away from the frontline BUT! prefer Cvs. Everybody likes CVs! @@ -556,9 +556,10 @@ class CoalitionMissionPlanner: # Find farthest, friendly CP for AEWC cp = self.objective_finder.farthest_friendly_control_point() - yield ProposedMission( - cp, [ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)] - ) + if cp is not None: + yield ProposedMission( + cp, [ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)] + ) # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. for cp in self.objective_finder.vulnerable_control_points(): @@ -589,9 +590,23 @@ class CoalitionMissionPlanner: front_line, [ ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE), - ProposedFlight( - FlightType.TARCAP, 2, self.MAX_CAP_RANGE, EscortType.AirToAir - ), + # This is *not* an escort because front lines don't create a threat + # zone. Generating threat zones from front lines causes the front + # line to push back BARCAPs as it gets closer to the base. While + # front lines do have the same problem of potentially pulling + # BARCAPs off bases to engage a front line TARCAP, that's probably + # the one time where we do want that. + # + # TODO: Use intercepts and extra TARCAPs to cover bases near fronts. + # We don't have intercept missions yet so this isn't something we + # can do today, but we should probably return to having the front + # line project a threat zone (so that strike missions will route + # around it) and instead *not plan* a BARCAP at bases near the + # front, since there isn't a place to put a barrier. Instead, the + # aircraft that would have been a BARCAP could be used as additional + # interceptors and TARCAPs which will defend the base but won't be + # trying to avoid front line contacts. + ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE), ], ) diff --git a/qt_ui/displayoptions.py b/qt_ui/displayoptions.py index 423fe77f..2abf369c 100644 --- a/qt_ui/displayoptions.py +++ b/qt_ui/displayoptions.py @@ -103,7 +103,9 @@ class DisplayOptions: waypoint_info = DisplayRule("Waypoint Information", True) culling = DisplayRule("Display Culling Zones", False) actual_frontline_pos = DisplayRule("Display Actual Frontline Location", False) - barcap_commit_range = DisplayRule("Display selected BARCAP commit range", False) + patrol_engagement_range = DisplayRule( + "Display selected patrol engagement range", True + ) flight_paths = FlightPathOptions() blue_threat_zones = ThreatZoneOptions("Blue") red_threat_zones = ThreatZoneOptions("Red") diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index d8b5f206..11705746 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -58,6 +58,8 @@ from gen.flights.flightplan import ( FlightPlan, FlightPlanBuilder, InvalidObjectiveLocation, + PatrollingFlightPlan, + TarCapFlightPlan, ) from gen.flights.traveltime import TotEstimator from qt_ui.displayoptions import DisplayOptions, ThreatZoneOptions @@ -721,13 +723,11 @@ class QLiberationMap(QGraphicsView): ) prev_pos = tuple(new_pos) - if selected and DisplayOptions.barcap_commit_range: - self.draw_barcap_commit_range(scene, flight) + if selected and DisplayOptions.patrol_engagement_range: + self.draw_patrol_commit_range(scene, flight) - def draw_barcap_commit_range(self, scene: QGraphicsScene, flight: Flight) -> None: - if flight.flight_type is not FlightType.BARCAP: - return - if not isinstance(flight.flight_plan, BarCapFlightPlan): + def draw_patrol_commit_range(self, scene: QGraphicsScene, flight: Flight) -> None: + if not isinstance(flight.flight_plan, PatrollingFlightPlan): return start = flight.flight_plan.patrol_start end = flight.flight_plan.patrol_end diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 0ff28d59..6eaa93d4 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -223,7 +223,7 @@ class FactionSelection(QtWidgets.QWizardPage): self.redFactionSelect.activated.connect(self.updateUnitRecap) def setDefaultFactions(self, campaign: Campaign): - """ Set default faction for selected campaign """ + """Set default faction for selected campaign""" self.blueFactionSelect.clear() self.redFactionSelect.clear() diff --git a/resources/campaigns/full_caucasus.miz b/resources/campaigns/full_caucasus.miz index cbdc9dc7..3f27702c 100644 Binary files a/resources/campaigns/full_caucasus.miz and b/resources/campaigns/full_caucasus.miz differ