diff --git a/game/data/building_data.py b/game/data/building_data.py index 5688fcb8..c3bd62d9 100644 --- a/game/data/building_data.py +++ b/game/data/building_data.py @@ -3,7 +3,6 @@ import dcs DEFAULT_AVAILABLE_BUILDINGS = [ "fuel", - "ammo", "comms", "oil", "ware", diff --git a/game/procurement.py b/game/procurement.py index 0fe0e833..b7222dc4 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -262,10 +262,19 @@ class ProcurementAi: # Prefer to buy front line units at active front lines that are not # already overloaded. for cp in self.owned_points: + + total_ground_units_allocated_to_this_control_point = ( + self.total_ground_units_allocated_to(cp) + ) + if not cp.has_ground_unit_source(self.game): continue - if self.total_ground_units_allocated_to(cp) >= 50: + if ( + total_ground_units_allocated_to_this_control_point >= 50 + or total_ground_units_allocated_to_this_control_point + >= cp.frontline_unit_count_limit + ): # Control point is already sufficiently defended. continue for connected in cp.connected_points: diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 26192cb7..e62a39f0 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -22,7 +22,7 @@ from dcs.ships import ( DDG_Arleigh_Burke_IIa, LHA_1_Tarawa, ) -from dcs.statics import Fortification +from dcs.statics import Fortification, Warehouse from dcs.terrain import ( caucasus, nevada, @@ -128,6 +128,8 @@ class MizCampaignLoader: FACTORY_UNIT_TYPE = Fortification.Workshop_A.id + AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id + REQUIRED_STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id BASE_DEFENSE_RADIUS = nautical_miles(2) @@ -320,6 +322,12 @@ class MizCampaignLoader: if group.units[0].type in self.FACTORY_UNIT_TYPE: yield group + @property + def ammunition_depots(self) -> Iterator[StaticGroup]: + for group in itertools.chain(self.blue.static_group, self.red.static_group): + if group.units[0].type in self.AMMUNITION_DEPOT_UNIT_TYPE: + yield group + @property def required_strike_targets(self) -> Iterator[StaticGroup]: for group in itertools.chain(self.blue.static_group, self.red.static_group): @@ -559,6 +567,12 @@ class MizCampaignLoader: PointWithHeading.from_point(group.position, group.units[0].heading) ) + for group in self.ammunition_depots: + closest, distance = self.objective_info(group) + closest.preset_locations.ammunition_depots.append( + PointWithHeading.from_point(group.position, group.units[0].heading) + ) + for group in self.required_strike_targets: closest, distance = self.objective_info(group) closest.preset_locations.required_strike_locations.append( diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index bfa707d3..a9312545 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -59,6 +59,9 @@ if TYPE_CHECKING: from game import Game from gen.flights.flight import FlightType +FREE_FRONTLINE_UNIT_SUPPLY: int = 15 +AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12 + class ControlPointType(Enum): #: An airbase with slots for everything. @@ -161,6 +164,9 @@ class PresetLocations: #: Locations of factories for producing ground units. These will always be spawned. factories: List[PointWithHeading] = field(default_factory=list) + #: Locations of ammo depots for controlling number of units on the front line at a control point. + ammunition_depots: List[PointWithHeading] = field(default_factory=list) + #: Locations of stationary armor groups. These will always be spawned. armor_groups: List[PointWithHeading] = field(default_factory=list) @@ -783,6 +789,20 @@ class ControlPoint(MissionTarget, ABC): return self.captured != other.captured + @property + def frontline_unit_count_limit(self) -> int: + + tally_connected_ammo_depots = 0 + + for cp_objective in self.connected_objectives: + if cp_objective.category == "ammo" and not cp_objective.is_dead: + tally_connected_ammo_depots += 1 + + return ( + FREE_FRONTLINE_UNIT_SUPPLY + + tally_connected_ammo_depots * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION + ) + @property def strike_targets(self) -> List[Union[MissionTarget, Unit]]: return [] diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 2fbb7c57..88ad2bcd 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -471,6 +471,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): self.generate_strike_targets() self.generate_offshore_strike_targets() self.generate_factories() + self.generate_ammunition_depots() if self.faction.missiles: self.generate_missile_sites() @@ -629,6 +630,10 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): self.control_point.connected_objectives.append(g) + def generate_ammunition_depots(self) -> None: + for position in self.control_point.preset_locations.ammunition_depots: + self.generate_strike_target_at(category="ammo", position=position) + def generate_factories(self) -> None: """Generates the factories that are required by the campaign.""" for position in self.control_point.preset_locations.factories: @@ -828,6 +833,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator): FobDefenseGenerator(self.game, self.control_point).generate() self.generate_armor_groups() self.generate_factories() + self.generate_ammunition_depots() self.generate_required_aa() self.generate_required_ewr() self.generate_scenery_sites() diff --git a/game/version.py b/game/version.py index 2a9cebbb..564c43e3 100644 --- a/game/version.py +++ b/game/version.py @@ -73,4 +73,12 @@ VERSION = _build_version_string() #: * AAA_8_8cm_Flak_18, #: * SPAAA_Vulcan_M163, #: * SPAAA_ZSU_23_4_Shilka_Gun_Dish, -CAMPAIGN_FORMAT_VERSION = (4, 2) +#: +#: Version 5.0 +#: * Ammunition Depots objective locations are now predetermined using the "Ammunition Depot" +#: Warehouse object, and through trigger zone based scenery objects. +#: * The number of alive Ammunition Depot objective buildings connected to a control point +#: directly influences how many ground units can be supported on the front line. +#: * The number of supported ground units at any control point is artificially capped at 50, +#: even if the number of alive Ammunition Depot objectives can support more. +CAMPAIGN_FORMAT_VERSION = (5, 0) diff --git a/gen/ground_forces/ai_ground_planner.py b/gen/ground_forces/ai_ground_planner.py index 777efd01..761bf76b 100644 --- a/gen/ground_forces/ai_ground_planner.py +++ b/gen/ground_forces/ai_ground_planner.py @@ -80,6 +80,10 @@ class GroundPlanner: def plan_groundwar(self): + ground_unit_limit = self.cp.frontline_unit_count_limit + + remaining_available_frontline_units = ground_unit_limit + if hasattr(self.cp, "stance"): group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance] else: @@ -118,6 +122,12 @@ class GroundPlanner: continue available = self.cp.base.armor[key] + + if available > remaining_available_frontline_units: + available = remaining_available_frontline_units + + remaining_available_frontline_units -= available + while available > 0: if role == CombatGroupRole.SHORAD: @@ -144,6 +154,9 @@ class GroundPlanner: group.units.append(key) collection.append(group) + if remaining_available_frontline_units == 0: + break + print("------------------") print("Ground Planner : ") print(self.cp.name)