Add support for required variants of all TGOs.

Adds required variants of:

* SHORADS
* Armor groups
* Buildings
* Oil rigs
* Coastal defenses
* Missile sites
* Ships

This is prep work for removing random generation.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1076
This commit is contained in:
Dan Albert
2021-05-17 22:24:31 -07:00
parent 8076206a90
commit 739406614d
5 changed files with 250 additions and 28 deletions

View File

@@ -40,8 +40,6 @@ from dcs.unitgroup import (
VehicleGroup,
)
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from dcs.triggers import Triggers
from dcs.triggers import TriggerZone
from ..scenery_group import SceneryGroup
from pyproj import CRS, Transformer
@@ -110,10 +108,21 @@ class MizCampaignLoader:
AirDefence.SAM_SA_3_S_125_Goa_LN.id,
}
REQUIRED_SHORT_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Avenger__Stinger.id,
AirDefence.SAM_Rapier_LN.id,
AirDefence.SAM_SA_19_Tunguska_Grison.id,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id,
}
REQUIRED_EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
REQUIRED_STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
BASE_DEFENSE_RADIUS = nautical_miles(2)
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
@@ -196,6 +205,12 @@ class MizCampaignLoader:
if group.units[0].type == self.SHIP_UNIT_TYPE:
yield group
@property
def required_ships(self) -> Iterator[ShipGroup]:
for group in self.red.ship_group:
if group.units[0].type == self.SHIP_UNIT_TYPE:
yield group
@property
def ewrs(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
@@ -220,18 +235,36 @@ class MizCampaignLoader:
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
yield group
@property
def required_offshore_strike_targets(self) -> Iterator[StaticGroup]:
for group in self.red.static_group:
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
yield group
@property
def missile_sites(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
yield group
@property
def required_missile_sites(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
yield group
@property
def coastal_defenses(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
yield group
@property
def required_coastal_defenses(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
yield group
@property
def required_long_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
@@ -244,12 +277,24 @@ class MizCampaignLoader:
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
yield group
@property
def required_short_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_SHORT_RANGE_SAM_UNIT_TYPES:
yield group
@property
def required_ewrs(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_EWR_UNIT_TYPE:
yield group
@property
def armor_groups(self) -> Iterator[VehicleGroup]:
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
if group.units[0].type in self.ARMOR_GROUP_UNIT_TYPE:
yield group
@property
def helipads(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
@@ -262,6 +307,12 @@ class MizCampaignLoader:
if group.units[0].type in self.FACTORY_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):
if group.units[0].type in self.REQUIRED_STRIKE_TARGET_UNIT_TYPE:
yield group
@property
def scenery(self) -> List[SceneryGroup]:
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
@@ -405,24 +456,48 @@ class MizCampaignLoader:
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_offshore_strike_targets:
closest, distance = self.objective_info(group)
closest.preset_locations.required_offshore_strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.ships:
closest, distance = self.objective_info(group)
closest.preset_locations.ships.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_ships:
closest, distance = self.objective_info(group)
closest.preset_locations.required_ships.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.missile_sites:
closest, distance = self.objective_info(group)
closest.preset_locations.missile_sites.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_missile_sites:
closest, distance = self.objective_info(group)
closest.preset_locations.required_missile_sites.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.coastal_defenses:
closest, distance = self.objective_info(group)
closest.preset_locations.coastal_defenses.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_coastal_defenses:
closest, distance = self.objective_info(group)
closest.preset_locations.required_coastal_defenses.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_long_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.required_long_range_sams.append(
@@ -435,12 +510,24 @@ class MizCampaignLoader:
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_short_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.required_short_range_sams.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_ewrs:
closest, distance = self.objective_info(group)
closest.preset_locations.required_ewrs.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.armor_groups:
closest, distance = self.objective_info(group)
closest.preset_locations.armor_groups.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.helipads:
closest, distance = self.objective_info(group)
closest.helipads.append(
@@ -453,6 +540,12 @@ class MizCampaignLoader:
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(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.scenery:
closest, distance = self.objective_info(group)
closest.preset_locations.scenery.append(group)

View File

@@ -94,24 +94,47 @@ class PresetLocations:
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
ships: List[PointWithHeading] = field(default_factory=list)
#: Locations used by non-carrier ships that will be spawned unless the faction has
#: no navy or the player has disable ship generation for the original owning side.
required_ships: List[PointWithHeading] = field(default_factory=list)
#: Locations used by coastal defenses.
coastal_defenses: List[PointWithHeading] = field(default_factory=list)
#: Locations used by coastal defenses that are always generated if the faction is
#: capable.
required_coastal_defenses: List[PointWithHeading] = field(default_factory=list)
#: Locations used by ground based strike objectives.
strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by ground based strike objectives that will always be spawned.
required_strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by offshore strike objectives.
offshore_strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by offshore strike objectives that will always be spawned.
required_offshore_strike_locations: List[PointWithHeading] = field(
default_factory=list
)
#: Locations used by missile sites like scuds and V-2s.
missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations used by missile sites like scuds and V-2s that are always generated if
#: the faction is capable.
required_missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations of long range SAMs which should always be spawned.
required_long_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of medium range SAMs which should always be spawned.
required_medium_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of short range SAMs which should always be spawned.
required_short_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of EWRs which should always be spawned.
required_ewrs: List[PointWithHeading] = field(default_factory=list)
@@ -121,6 +144,9 @@ class PresetLocations:
#: Locations of factories for producing ground units. These will always be spawned.
factories: List[PointWithHeading] = field(default_factory=list)
#: Locations of stationary armor groups. These will always be spawned.
armor_groups: 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."""

View File

@@ -188,10 +188,7 @@ class ControlPointGroundObjectGenerator:
def generate(self) -> bool:
self.control_point.connected_objectives = []
if self.faction.navy_generators:
# Even airbases can generate navies if they are close enough to the
# water. This is not controlled by the control point definition, but
# rather by whether or not the generator can find a valid position
# for the ship.
# Even airbases can generate navies if they are close enough to the water.
self.generate_navy()
return True
@@ -205,18 +202,24 @@ class ControlPointGroundObjectGenerator:
if not self.control_point.captured and skip_enemy_navy:
return
self.generate_required_ships()
for _ in range(self.faction.navy_group_count):
self.generate_ship()
def generate_required_ships(self) -> None:
for position in self.control_point.preset_locations.required_ships:
self.generate_ship_at(position)
def generate_ship(self) -> None:
point = self.location_finder.location_for(LocationType.Ship)
if point is None:
return
if point is not None:
self.generate_ship_at(point)
def generate_ship_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = ShipGroundObject(
namegen.random_objective_name(), group_id, point, self.control_point
namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_ship_group(self.game, g, self.faction_name)
@@ -453,21 +456,26 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
BaseDefenseGenerator(self.game, self.control_point).generate()
self.generate_ground_points()
if self.faction.missiles:
self.generate_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
return True
def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point."""
self.generate_armor_groups()
skip_sams = self.generate_required_aa()
skip_ewrs = self.generate_required_ewr()
self.generate_scenery_sites()
self.generate_strike_targets()
self.generate_offshore_strike_targets()
self.generate_factories()
if self.faction.missiles:
self.generate_missile_sites()
self.generate_required_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
self.generate_required_coastal_sites()
if self.control_point.is_global:
return
@@ -492,6 +500,32 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
else:
self.generate_ground_point()
def generate_armor_groups(self) -> None:
for position in self.control_point.preset_locations.armor_groups:
self.generate_armor_at(position)
def generate_armor_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = VehicleGroupGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=False,
)
group = generate_armor_group(self.faction_name, self.game, g)
if group is None:
logging.error(
"Could not generate armor group for %s at %s",
g.name,
self.control_point,
)
return
g.groups = [group]
self.control_point.connected_objectives.append(g)
def generate_required_aa(self) -> int:
"""Generates the AA sites that are required by the campaign.
@@ -516,8 +550,15 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
{AirDefenseRange.Short},
],
)
return len(presets.required_long_range_sams) + len(
presets.required_medium_range_sams
for position in presets.required_short_range_sams:
self.generate_aa_at(
position,
ranges=[{AirDefenseRange.Short}],
)
return (
len(presets.required_long_range_sams)
+ len(presets.required_medium_range_sams)
+ len(presets.required_short_range_sams)
)
def generate_required_ewr(self) -> int:
@@ -538,9 +579,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
logging.exception("Faction has no buildings defined")
return
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
if category == "oil":
location_type = LocationType.OffshoreStrikeTarget
else:
@@ -551,6 +589,13 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
if point is None:
return
self.generate_strike_target_at(category, point)
def generate_strike_target_at(self, category: str, position: Point) -> None:
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
object_id = 0
group_id = self.game.next_group_id()
@@ -564,7 +609,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
category,
group_id,
object_id,
point + template_point,
position + template_point,
unit["heading"],
self.control_point,
unit["type"],
@@ -633,7 +678,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return
self.generate_ewr_at(position)
def generate_ewr_at(self, position: Point) -> None:
def generate_ewr_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = EwrGroundObject(
@@ -688,15 +733,20 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return
def generate_required_missile_sites(self) -> None:
for position in self.control_point.preset_locations.required_missile_sites:
self.generate_missile_site_at(position)
def generate_missile_sites(self) -> None:
for i in range(self.faction.missiles_group_count):
self.generate_missile_site()
def generate_missile_site(self) -> None:
position = self.location_finder.location_for(LocationType.MissileSite)
if position is None:
return
if position is not None:
return self.generate_missile_site_at(position)
def generate_missile_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = MissileSiteGroundObject(
@@ -709,15 +759,20 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
return
def generate_required_coastal_sites(self) -> None:
for position in self.control_point.preset_locations.required_coastal_defenses:
self.generate_coastal_site_at(position)
def generate_coastal_sites(self) -> None:
for i in range(self.faction.coastal_group_count):
self.generate_coastal_site()
def generate_coastal_site(self) -> None:
position = self.location_finder.location_for(LocationType.Coastal)
if position is None:
return
if position is not None:
self.generate_coastal_site_at(position)
def generate_coastal_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = CoastalSiteGroundObject(
@@ -734,13 +789,47 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
return
def generate_strike_targets(self) -> None:
"""Generates the strike targets that are required by the campaign."""
building_set = list(set(self.faction.building_set) - {"oil"})
if not building_set:
logging.error("Faction has no buildings defined")
return
for position in self.control_point.preset_locations.required_strike_locations:
category = random.choice(building_set)
self.generate_strike_target_at(category, position)
def generate_offshore_strike_targets(self) -> None:
"""Generates the offshore strike targets that are required by the campaign."""
if "oil" not in self.faction.building_set:
logging.error("Faction does not support offshore strike targets")
return
for (
position
) in self.control_point.preset_locations.required_offshore_strike_locations:
self.generate_strike_target_at("oil", position)
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate(self) -> bool:
self.generate_fob()
FobDefenseGenerator(self.game, self.control_point).generate()
self.generate_armor_groups()
self.generate_factories()
self.generate_required_aa()
self.generate_required_ewr()
self.generate_scenery_sites()
self.generate_strike_targets()
self.generate_offshore_strike_targets()
if self.faction.missiles:
self.generate_missile_sites()
self.generate_required_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
self.generate_required_coastal_sites()
return True
def generate_fob(self) -> None:

View File

@@ -53,4 +53,17 @@ VERSION = _build_version_string()
#: by a blue circular TriggerZone, campaign creation will fail. Blue circular
#: TriggerZones must also have their first property's value field define the type of
#: objective (a valid value for a building TGO category, from `game.db.PRICES`).
CAMPAIGN_FORMAT_VERSION = (4, 0)
#:
#: Version 4.1
#: * All objective types may now be set as required generation (similar to the required
#: IADS generation). This includes:
#: * SHORADS
#: * Armor groups
#: * Strike targets
#: * Offshore strike targets
#: * Ships
#: * Missile sites
#: * Coastal defenses
#:
#: See the unit lists in MizCampaignLoader in conflicttheater.py for unit types.
CAMPAIGN_FORMAT_VERSION = (4, 1)