diff --git a/changelog.md b/changelog.md index 3338fab1..c9d21aca 100644 --- a/changelog.md +++ b/changelog.md @@ -27,6 +27,7 @@ * **[Campaign Design]** Ability to define designated CTLD zones for Control Points (Airbases & FOBs/FARPs) * **[Campaign Design]** Ability to define preset groups for specific TGOs, given the preset group is accessible for the faction and the task matches. * **[Campaign Management]** Additional options for automated budget management. +* **[Campaign Management]** New options to allow more control of randomized flight sizes (applicable for BARCAP/CAS/OCA/ANTI-SHIP). ## Fixes * **[UI]** Removed deprecated options diff --git a/game/commander/tasks/packageplanningtask.py b/game/commander/tasks/packageplanningtask.py index d24492a8..dad8c0c4 100644 --- a/game/commander/tasks/packageplanningtask.py +++ b/game/commander/tasks/packageplanningtask.py @@ -183,3 +183,12 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]): if iads_threat not in state.threatening_air_defenses: state.threatening_air_defenses.append(iads_threat) return not threatened + + def get_flight_size(self) -> int: + settings = self.target.coalition.game.settings + weights = [ + settings.fpa_2ship_weight, + settings.fpa_3ship_weight, + settings.fpa_4ship_weight, + ] + return random.choices([2, 3, 4], weights, k=1)[0] diff --git a/game/commander/tasks/primitive/antiship.py b/game/commander/tasks/primitive/antiship.py index e90800eb..95a2c752 100644 --- a/game/commander/tasks/primitive/antiship.py +++ b/game/commander/tasks/primitive/antiship.py @@ -23,5 +23,6 @@ class PlanAntiShip(PackagePlanningTask[NavalGroundObject]): state.eliminate_ship(self.target) def propose_flights(self) -> None: - self.propose_flight(FlightType.ANTISHIP, randint(2, 4)) + size = self.get_flight_size() + self.propose_flight(FlightType.ANTISHIP, size) self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir) diff --git a/game/commander/tasks/primitive/antishipping.py b/game/commander/tasks/primitive/antishipping.py index fa084c4e..8a65a7c8 100644 --- a/game/commander/tasks/primitive/antishipping.py +++ b/game/commander/tasks/primitive/antishipping.py @@ -22,5 +22,6 @@ class PlanAntiShipping(PackagePlanningTask[CargoShip]): state.enemy_shipping.remove(self.target) def propose_flights(self) -> None: - self.propose_flight(FlightType.ANTISHIP, randint(2, 4)) + size = self.get_flight_size() + self.propose_flight(FlightType.ANTISHIP, size) self.propose_common_escorts() diff --git a/game/commander/tasks/primitive/barcap.py b/game/commander/tasks/primitive/barcap.py index f83f9c54..2ba674f9 100644 --- a/game/commander/tasks/primitive/barcap.py +++ b/game/commander/tasks/primitive/barcap.py @@ -1,5 +1,6 @@ from __future__ import annotations +import random from dataclasses import dataclass from random import randint @@ -22,7 +23,8 @@ class PlanBarcap(PackagePlanningTask[ControlPoint]): state.barcaps_needed[self.target] -= 1 def propose_flights(self) -> None: - self.propose_flight(FlightType.BARCAP, randint(2, 4)) + size = self.get_flight_size() + self.propose_flight(FlightType.BARCAP, size) @property def purchase_multiplier(self) -> int: diff --git a/game/commander/tasks/primitive/cas.py b/game/commander/tasks/primitive/cas.py index 3f036c2b..597e293d 100644 --- a/game/commander/tasks/primitive/cas.py +++ b/game/commander/tasks/primitive/cas.py @@ -30,5 +30,6 @@ class PlanCas(PackagePlanningTask[FrontLine]): state.vulnerable_front_lines.remove(self.target) def propose_flights(self) -> None: - self.propose_flight(FlightType.CAS, randint(2, 4)) + size = self.get_flight_size() + self.propose_flight(FlightType.CAS, size) self.propose_flight(FlightType.TARCAP, 2) diff --git a/game/commander/tasks/primitive/oca.py b/game/commander/tasks/primitive/oca.py index bbe73e15..298fae1f 100644 --- a/game/commander/tasks/primitive/oca.py +++ b/game/commander/tasks/primitive/oca.py @@ -24,7 +24,8 @@ class PlanOcaStrike(PackagePlanningTask[ControlPoint]): state.oca_targets.remove(self.target) def propose_flights(self) -> None: - self.propose_flight(FlightType.OCA_RUNWAY, randint(2, 4)) + size = self.get_flight_size() + self.propose_flight(FlightType.OCA_RUNWAY, size) if self.aircraft_cold_start: self.propose_flight(FlightType.OCA_AIRCRAFT, 2) self.propose_common_escorts() diff --git a/game/settings/settings.py b/game/settings/settings.py index 10a37c89..7b16cd83 100644 --- a/game/settings/settings.py +++ b/game/settings/settings.py @@ -36,6 +36,7 @@ CAMPAIGN_MANAGEMENT_PAGE = "Campaign Management" GENERAL_SECTION = "General" PILOTS_AND_SQUADRONS_SECTION = "Pilots and Squadrons" HQ_AUTOMATION_SECTION = "HQ Automation" +FLIGHT_PLANNER_AUTOMATION = "Flight Planner Automation" MISSION_GENERATOR_PAGE = "Mission Generator" @@ -310,6 +311,41 @@ class Settings: ), ) + # Flight Planner Automation + #: The weight used for 2-ships. + fpa_2ship_weight: int = bounded_int_option( + "2-ship weight factor (WF2)", + CAMPAIGN_MANAGEMENT_PAGE, + FLIGHT_PLANNER_AUTOMATION, + default=50, + min=0, + max=100, + detail=( + "Used as a distribution to randomize 2/3/4-ships for BARCAP, CAS, OCA & ANTI-SHIP flights. " + "The weight W_i is calculated according to the following formula: " + "W_i = WF_i / (WF2 + WF3 + WF4)" + ), + ) + #: The weight used for 3-ships. + fpa_3ship_weight: int = bounded_int_option( + "3-ship weight factor (WF3)", + CAMPAIGN_MANAGEMENT_PAGE, + FLIGHT_PLANNER_AUTOMATION, + default=35, + min=0, + max=100, + detail="See 2-ship weight factor (WF2)", + ) + fpa_4ship_weight: int = bounded_int_option( + "4-ship weight factor (WF4)", + CAMPAIGN_MANAGEMENT_PAGE, + FLIGHT_PLANNER_AUTOMATION, + default=15, + min=0, + max=100, + detail="See 2-ship weight factor (WF2)", + ) + # Mission Generator # Gameplay fast_forward_to_first_contact: bool = boolean_option( diff --git a/game/theater/frontline.py b/game/theater/frontline.py index 82173cfd..8b13b302 100644 --- a/game/theater/frontline.py +++ b/game/theater/frontline.py @@ -12,7 +12,7 @@ from ..utils import Heading, pairwise if TYPE_CHECKING: from game.ato import FlightType - from .controlpoint import ControlPoint + from .controlpoint import ControlPoint, Coalition FRONTLINE_MIN_CP_DISTANCE = 5000 @@ -121,6 +121,10 @@ class FrontLine(MissionTarget): """Returns a tuple of the two control points.""" return self.blue_cp, self.red_cp + @property + def coalition(self) -> Coalition: + return self.blue_cp.coalition + @property def route_length(self) -> float: """The total distance of all segments""" diff --git a/game/theater/missiontarget.py b/game/theater/missiontarget.py index dbb7241b..fb48d6f0 100644 --- a/game/theater/missiontarget.py +++ b/game/theater/missiontarget.py @@ -6,7 +6,7 @@ from dcs.mapping import Point if TYPE_CHECKING: from game.ato.flighttype import FlightType - from game.theater import TheaterUnit + from game.theater import TheaterUnit, Coalition class MissionTarget: @@ -47,3 +47,7 @@ class MissionTarget: @property def strike_targets(self) -> list[TheaterUnit]: return [] + + @property + def coalition(self) -> Coalition: + raise NotImplementedError diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 479a8144..acca4417 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -27,7 +27,7 @@ if TYPE_CHECKING: from game.ato.flighttype import FlightType from game.threatzones import ThreatPoly from .theatergroup import TheaterUnit, TheaterGroup - from .controlpoint import ControlPoint + from .controlpoint import ControlPoint, Coalition NAME_BY_CATEGORY = { @@ -275,6 +275,10 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC): def is_iads(self) -> bool: return False + @property + def coalition(self) -> Coalition: + return self.control_point.coalition + class BuildingGroundObject(TheaterGroundObject): def __init__( diff --git a/game/transfers.py b/game/transfers.py index b755f4b9..a0014cf7 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -59,6 +59,7 @@ from game.utils import meters, nautical_miles if TYPE_CHECKING: from game import Game from game.squadrons import Squadron + from game.theater import Coalition class Transport: @@ -432,6 +433,10 @@ class MultiGroupTransport(MissionTarget, Transport): def description(self) -> str: raise NotImplementedError + @property + def coalition(self) -> Coalition: + return self.origin.coalition + class Convoy(MultiGroupTransport): def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None: