diff --git a/changelog.md b/changelog.md index 59ee06e0..71823de9 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ Saves from 2.5 are not compatible with 2.6. * **[Campaign]** Ground units can now be transferred by road. See https://github.com/Khopa/dcs_liberation/wiki/Unit-Transfers for more information. * **[Campaign]** Ground units can no longer be sold. To move units to a new location, transfer them. * **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present. +* **[Modding]** Campaigns now choose locations for factories to spawn. ## Fixes diff --git a/game/data/building_data.py b/game/data/building_data.py index 5331ebbd..e6cd4ba9 100644 --- a/game/data/building_data.py +++ b/game/data/building_data.py @@ -10,14 +10,12 @@ DEFAULT_AVAILABLE_BUILDINGS = [ "farp", "fob", "power", - "factory", "derrick", ] -WW2_FREE = ["fuel", "factory", "ware", "fob"] +WW2_FREE = ["fuel", "ware", "fob"] WW2_GERMANY_BUILDINGS = [ "fuel", - "factory", "ww2bunker", "ww2bunker", "ww2bunker", @@ -27,7 +25,6 @@ WW2_GERMANY_BUILDINGS = [ ] WW2_ALLIES_BUILDINGS = [ "fuel", - "factory", "allycamp", "allycamp", "allycamp", diff --git a/game/db.py b/game/db.py index c091a399..d1cd9029 100644 --- a/game/db.py +++ b/game/db.py @@ -1350,6 +1350,7 @@ REWARDS = { "ammo": 2, "farp": 1, "fob": 1, + # TODO: Should generate no cash once they generate units. "factory": 10, "comms": 10, "oil": 10, diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 80194189..33a48b8c 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -120,6 +120,8 @@ class MizCampaignLoader: REQUIRED_EWR_UNIT_TYPE = AirDefence.EWR_1L13.id + FACTORY_UNIT_TYPE = Fortification.Workshop_A.id + BASE_DEFENSE_RADIUS = nautical_miles(2) def __init__(self, miz: Path, theater: ConflictTheater) -> None: @@ -261,6 +263,12 @@ class MizCampaignLoader: if group.units[0].type == self.FARP_HELIPAD: yield group + @property + def factories(self) -> Iterator[StaticGroup]: + for group in self.blue.static_group: + if group.units[0].type in self.FACTORY_UNIT_TYPE: + yield group + @cached_property def control_points(self) -> Dict[int, ControlPoint]: control_points = {} @@ -421,6 +429,12 @@ class MizCampaignLoader: PointWithHeading.from_point(group.position, group.units[0].heading) ) + for group in self.factories: + closest, distance = self.objective_info(group) + closest.preset_locations.factories.append( + PointWithHeading.from_point(group.position, group.units[0].heading) + ) + def populate_theater(self) -> None: for control_point in self.control_points.values(): self.theater.add_controlpoint(control_point) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 69b9db2f..45477bc0 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -110,6 +110,9 @@ class PresetLocations: #: Locations of EWRs which should always be spawned. required_ewrs: List[PointWithHeading] = field(default_factory=list) + #: Locations of factories for producing ground units. These will always be spawned. + factories: List[PointWithHeading] = field(default_factory=list) + @staticmethod def _random_from(points: List[PointWithHeading]) -> Optional[PointWithHeading]: """Finds, removes, and returns a random position from the given list.""" diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 5316b9c8..9dd6e05c 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -18,6 +18,7 @@ from game.theater.theatergroundobject import ( BuildingGroundObject, CarrierGroundObject, EwrGroundObject, + FactoryGroundObject, LhaGroundObject, MissileSiteGroundObject, SamGroundObject, @@ -612,6 +613,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): """Generate ground objects and AA sites for the control point.""" skip_sams = self.generate_required_aa() skip_ewrs = self.generate_required_ewr() + self.generate_factories() if self.control_point.is_global: return @@ -717,6 +719,25 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): self.control_point.connected_objectives.append(g) + def generate_factories(self) -> None: + """Generates the factories that are required by the campaign.""" + for position in self.control_point.preset_locations.factories: + self.generate_factory_at(position) + + def generate_factory_at(self, point: PointWithHeading) -> None: + obj_name = namegen.random_objective_name() + group_id = self.game.next_group_id() + + g = FactoryGroundObject( + obj_name, + group_id, + point, + point.heading, + self.control_point, + ) + + self.control_point.connected_objectives.append(g) + def generate_aa_site(self) -> None: position = self.location_finder.location_for(LocationType.Sam) if position is None: diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index c476a90b..949e93a7 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -266,6 +266,28 @@ class BuildingGroundObject(TheaterGroundObject): self._dead = True +class FactoryGroundObject(BuildingGroundObject): + def __init__( + self, + name: str, + group_id: int, + position: Point, + heading: int, + control_point: ControlPoint, + ) -> None: + super().__init__( + name=name, + category="factory", + group_id=group_id, + object_id=0, + position=position, + heading=heading, + control_point=control_point, + dcs_identifier="Workshop A", + airbase_group=False, + ) + + class NavalGroundObject(TheaterGroundObject): def mission_types(self, for_player: bool) -> Iterator[FlightType]: from gen.flights.flight import FlightType diff --git a/game/version.py b/game/version.py index 9bb8c434..dc69650f 100644 --- a/game/version.py +++ b/game/version.py @@ -24,4 +24,19 @@ VERSION = _build_version_string() #: #: There is no verification that the campaign author updated their campaign correctly #: this is just a UI hint. +#: +#: Version history: +#: +#: Version 0 +#: * Unknown compatibility. +#: +#: Version 1 +#: * Compatible with Liberation 2.5. +#: +#: Version 2 +#: * Front line endpoints now define convoy origin/destination waypoints. They should be +#: placed on or near roads. +#: * Factories (Warehouse_A) define factory objectives. Only control points with +#: factories will be able to recruit ground units, so they should exist in sufficient +#: number and be protected by IADS. CAMPAIGN_FORMAT_VERSION = 1 diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 589bd960..f2c5fa6f 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -14,7 +14,7 @@ from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List from dcs import Mission, Point, unitgroup from dcs.country import Country from dcs.point import StaticPoint -from dcs.statics import fortification_map, warehouse_map, Warehouse +from dcs.statics import Fortification, fortification_map, warehouse_map, Warehouse from dcs.task import ( ActivateBeaconCommand, ActivateICLSCommand, @@ -34,6 +34,7 @@ from game.theater import ControlPoint, TheaterGroundObject from game.theater.theatergroundobject import ( BuildingGroundObject, CarrierGroundObject, + FactoryGroundObject, GenericCarrierGroundObject, LhaGroundObject, ShipGroundObject, @@ -213,7 +214,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator): f"{self.ground_object.dcs_identifier} not found in static maps" ) - def generate_vehicle_group(self, unit_type: UnitType) -> None: + def generate_vehicle_group(self, unit_type: Type[UnitType]) -> None: if not self.ground_object.is_dead: group = self.m.vehicle_group( country=self.country, @@ -224,7 +225,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator): ) self._register_fortification(group) - def generate_static(self, static_type: StaticType) -> None: + def generate_static(self, static_type: Type[StaticType]) -> None: group = self.m.static_group( country=self.country, name=self.ground_object.group_name, @@ -244,6 +245,22 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator): self.unit_map.add_building(self.ground_object, building) +class FactoryGenerator(BuildingSiteGenerator): + """Generator for factory sites. + + Factory sites are the buildings that allow the recruitment of ground units. + Destroying these prevents the owner from recruiting ground units at the connected + control point. + """ + + def generate(self) -> None: + if self.game.position_culled(self.ground_object.position): + return + + # TODO: Faction specific? + self.generate_static(Fortification.Workshop_A) + + class GenericCarrierGenerator(GenericGroundObjectGenerator): """Base type for carrier group generation. @@ -557,7 +574,11 @@ class GroundObjectsGenerator: ).generate() for ground_object in cp.ground_objects: - if isinstance(ground_object, BuildingGroundObject): + if isinstance(ground_object, FactoryGroundObject): + generator = FactoryGenerator( + ground_object, country, self.game, self.m, self.unit_map + ) + elif isinstance(ground_object, BuildingGroundObject): generator = BuildingSiteGenerator( ground_object, country, self.game, self.m, self.unit_map ) diff --git a/resources/campaigns/inherent_resolve.json b/resources/campaigns/inherent_resolve.json index 67acba0a..a5f3f079 100644 --- a/resources/campaigns/inherent_resolve.json +++ b/resources/campaigns/inherent_resolve.json @@ -5,7 +5,7 @@ "recommended_player_faction": "USA 2005", "recommended_enemy_faction": "Insurgents (Hard)", "description": "
In this scenario, you start from Jordan, and have to fight your way through eastern Syria.
", - "version": 1, + "version": 2, "miz": "inherent_resolve.miz", "performance": 1 } \ No newline at end of file diff --git a/resources/campaigns/inherent_resolve.miz b/resources/campaigns/inherent_resolve.miz index 8652f8df..9fe585f8 100644 Binary files a/resources/campaigns/inherent_resolve.miz and b/resources/campaigns/inherent_resolve.miz differ