Added coastal defenses sites generator for Iran and China.

This commit is contained in:
Khopa 2021-02-05 00:15:06 +01:00
parent 643dd65113
commit 2a6e2d470d
23 changed files with 212 additions and 65 deletions

View File

@ -60,6 +60,9 @@ class Faction:
# Possible Missile site generators for this faction
missiles: List[str] = field(default_factory=list)
# Possible costal site generators for this faction
coastal_defenses: List[str] = field(default_factory=list)
# Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict)
@ -90,6 +93,9 @@ class Faction:
# How many missiles group should we try to generate per CP on startup for this faction
missiles_group_count: int = field(default=1)
# How many coastal group should we try to generate per CP on startup for this faction
coastal_group_count: int = field(default=1)
# Whether this faction has JTAC access
has_jtac: bool = field(default=False)
@ -153,6 +159,7 @@ class Faction:
faction.air_defenses.extend(json.get("shorads", []))
faction.missiles = json.get("missiles", [])
faction.coastal_defenses = json.get("coastal_defenses", [])
faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", [])
@ -173,6 +180,7 @@ class Faction:
faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1))
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
# Load doctrine
doctrine = json.get("doctrine", "modern")

View File

@ -0,0 +1,16 @@
from dcs import Point
class PointWithHeading(Point):
def __init__(self):
super(PointWithHeading, self).__init__(0, 0)
self.heading = 0
@staticmethod
def from_point(point: Point, heading: int):
p = PointWithHeading()
p.x = point.x
p.y = point.y
p.heading = heading
return p

View File

@ -55,6 +55,7 @@ from .controlpoint import (
Fob,
)
from .landmap import Landmap, load_landmap, poly_contains
from ..point_with_heading import PointWithHeading
from ..utils import Distance, meters, nautical_miles
Numeric = Union[int, float]
@ -71,6 +72,7 @@ IMPORTANCE_HIGH = 1.4
FRONTLINE_MIN_CP_DISTANCE = 5000
def pairwise(iterable):
"""
itertools recipe
@ -183,7 +185,7 @@ class MizCampaignLoader:
for group in self.country(blue).ship_group:
if group.units[0].type == self.LHA_UNIT_TYPE:
yield group
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
for group in self.country(blue).vehicle_group:
if group.units[0].type == self.FOB_UNIT_TYPE:
@ -297,7 +299,7 @@ class MizCampaignLoader:
# final waypoint at the destination CP. Intermediate waypoints
# define the curve of the front line.
waypoints = [p.position for p in group.points]
origin = self.theater.closest_control_point(waypoints[0])
origin = self.theater.closest_control_point(waypoints[0])
if origin is None:
raise RuntimeError(
f"No control point near the first waypoint of {group.name}")
@ -326,7 +328,8 @@ class MizCampaignLoader:
for group in self.garrisons:
closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_garrisons.append(group.position)
closest.preset_locations.base_garrisons.append(
PointWithHeading.from_point(group.position, group.units[0].heading))
else:
logging.warning(
f"Found garrison unit too far from base: {group.name}")
@ -334,42 +337,44 @@ class MizCampaignLoader:
for group in self.sams:
closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_air_defense.append(group.position)
closest.preset_locations.base_air_defense.append(
PointWithHeading.from_point(group.position, group.units[0].heading))
else:
closest.preset_locations.strike_locations.append(group.position)
closest.preset_locations.strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading))
for group in self.ewrs:
closest, distance = self.objective_info(group)
closest.preset_locations.ewrs.append(group.position)
closest.preset_locations.ewrs.append(PointWithHeading.from_point(group.position, group.units[0].heading))
for group in self.offshore_strike_targets:
closest, distance = self.objective_info(group)
closest.preset_locations.offshore_strike_locations.append(
group.position)
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(group.position)
closest.preset_locations.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(group.position)
closest.preset_locations.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(group.position)
closest.preset_locations.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(
group.position
)
PointWithHeading.from_point(group.position, group.units[0].heading))
for group in self.required_medium_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.required_medium_range_sams.append(
group.position
)
PointWithHeading.from_point(group.position, group.units[0].heading))
def populate_theater(self) -> None:
for control_point in self.control_points.values():
@ -486,8 +491,8 @@ class ConflictTheater:
for inclusion_zone in self.landmap.inclusion_zones:
nearest_pair = ops.nearest_points(point, inclusion_zone)
nearest_points.append(nearest_pair[1])
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
nearest_point = nearest_points[0] # type: geometry.Point
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
nearest_point = nearest_points[0] # type: geometry.Point
for pt in nearest_points[1:]:
distance = point.distance(pt)
if distance < min_distance:
@ -547,7 +552,7 @@ class ConflictTheater:
closest = conflict
closest_distance = distance
return closest
def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""
Returns a tuple of the two nearest opposing ControlPoints in theater.
@ -572,7 +577,7 @@ class ConflictTheater:
self.find_control_point_by_id(i)
for i
in min(all_cp_min_distances, key=all_cp_min_distances.get) # type: ignore
] # type: List[ControlPoint]
] # type: List[ControlPoint]
assert len(closest_opposing_cps) == 2
if closest_opposing_cps[0].captured:
return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
@ -613,7 +618,7 @@ class ConflictTheater:
cp.captured_invert = False
return cp
@staticmethod
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
theaters = {
@ -649,7 +654,7 @@ class ConflictTheater:
cps[l[1]].connect(cps[l[0]])
return t
class CaucasusTheater(ConflictTheater):
terrain = caucasus.Caucasus()
@ -788,10 +793,10 @@ class FrontLine(MissionTarget):
"""
def __init__(
self,
control_point_a: ControlPoint,
control_point_b: ControlPoint,
theater: ConflictTheater
self,
control_point_a: ControlPoint,
control_point_b: ControlPoint,
theater: ConflictTheater
) -> None:
self.control_point_a = control_point_a
self.control_point_b = control_point_b
@ -876,7 +881,7 @@ class FrontLine(MissionTarget):
according to the current strength of each control point
"""
total_strength = (
self.control_point_a.base.strength + self.control_point_b.base.strength
self.control_point_a.base.strength + self.control_point_b.base.strength
)
if self.control_point_a.base.strength == 0:
return self._adjust_for_min_dist(0)
@ -891,11 +896,11 @@ class FrontLine(MissionTarget):
constant of either end control point.
"""
if (distance > self.attack_distance / 2) and (
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
):
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
elif (distance < self.attack_distance / 2) and (
distance < FRONTLINE_MIN_CP_DISTANCE
distance < FRONTLINE_MIN_CP_DISTANCE
):
distance = FRONTLINE_MIN_CP_DISTANCE
return distance
@ -910,8 +915,8 @@ class FrontLine(MissionTarget):
)
complex_frontlines = self.theater.frontline_data
if (complex_frontlines) and (
(control_point_ids in complex_frontlines)
or (reversed_cp_ids in complex_frontlines)
(control_point_ids in complex_frontlines)
or (reversed_cp_ids in complex_frontlines)
):
# The frontline segments must be stored in the correct order for the distance algorithms to work.
# The points in the frontline are ordered from the id before the | to the id after.
@ -935,10 +940,9 @@ class FrontLine(MissionTarget):
)
)
@staticmethod
def load_json_frontlines(
theater: ConflictTheater
theater: ConflictTheater
) -> Optional[Dict[str, ComplexFrontLine]]:
"""Load complex frontlines from json"""
try:

View File

@ -4,7 +4,6 @@ import heapq
import itertools
import logging
import random
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
@ -28,6 +27,7 @@ from gen.ground_forces.combat_stance import CombatStance
from gen.runways import RunwayAssigner, RunwayData
from .base import Base
from .missiontarget import MissionTarget
from game.point_with_heading import PointWithHeading
from .theatergroundobject import (
BaseDefenseGroundObject,
EwrGroundObject,
@ -71,44 +71,43 @@ class LocationType(Enum):
Shorad = "SHORAD"
StrikeTarget = "strike target"
@dataclass
class PresetLocations:
"""Defines the preset locations loaded from the campaign mission file."""
#: Locations used for spawning ground defenses for bases.
base_garrisons: List[Point] = field(default_factory=list)
base_garrisons: List[PointWithHeading] = field(default_factory=list)
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
#: and SHORADs.
base_air_defense: List[Point] = field(default_factory=list)
base_air_defense: List[PointWithHeading] = field(default_factory=list)
#: Locations used by EWRs.
ewrs: List[Point] = field(default_factory=list)
ewrs: List[PointWithHeading] = field(default_factory=list)
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
ships: List[Point] = field(default_factory=list)
ships: List[PointWithHeading] = field(default_factory=list)
#: Locations used by coastal defenses.
coastal_defenses: List[Point] = field(default_factory=list)
coastal_defenses: List[PointWithHeading] = field(default_factory=list)
#: Locations used by ground based strike objectives.
strike_locations: List[Point] = field(default_factory=list)
strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by offshore strike objectives.
offshore_strike_locations: List[Point] = field(default_factory=list)
offshore_strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by missile sites like scuds and V-2s.
missile_sites: List[Point] = field(default_factory=list)
missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations of long range SAMs which should always be spawned.
required_long_range_sams: List[Point] = field(default_factory=list)
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[Point] = field(default_factory=list)
required_medium_range_sams: List[PointWithHeading] = field(default_factory=list)
@staticmethod
def _random_from(points: List[Point]) -> Optional[Point]:
def _random_from(points: List[PointWithHeading]) -> Optional[PointWithHeading]:
"""Finds, removes, and returns a random position from the given list."""
if not points:
return None
@ -116,7 +115,7 @@ class PresetLocations:
points.remove(point)
return point
def random_for(self, location_type: LocationType) -> Optional[Point]:
def random_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
"""Returns a position suitable for the given location type.
The location, if found, will be claimed by the caller and not available
@ -376,20 +375,18 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Should be Airbase specific.
def clear_base_defenses(self) -> None:
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(base_defense.position)
self.preset_locations.ewrs.append(p)
elif isinstance(base_defense, SamGroundObject):
self.preset_locations.base_air_defense.append(
base_defense.position)
self.preset_locations.base_air_defense.append(p)
elif isinstance(base_defense, VehicleGroupGroundObject):
self.preset_locations.base_garrisons.append(
base_defense.position)
self.preset_locations.base_garrisons.append(p)
else:
logging.error(
"Could not determine preset location type for "
f"{base_defense}. Assuming garrison type.")
self.preset_locations.base_garrisons.append(
base_defense.position)
self.preset_locations.base_garrisons.append(p)
self.base_defenses = []
def capture_equipment(self, game: Game) -> None:

View File

@ -13,7 +13,7 @@ from dcs.vehicles import AirDefence
from game import Game, db
from game.factions.faction import Faction
from game.theater import Carrier, Lha, LocationType
from game.theater import Carrier, Lha, LocationType, PointWithHeading
from game.theater.theatergroundobject import (
BuildingGroundObject,
CarrierGroundObject,
@ -22,10 +22,11 @@ from game.theater.theatergroundobject import (
MissileSiteGroundObject,
SamGroundObject,
ShipGroundObject,
VehicleGroupGroundObject,
VehicleGroupGroundObject, CoastalSiteGroundObject,
)
from game.version import VERSION
from gen import namegen
from gen.coastal.coastal_group_generator import generate_coastal_group
from gen.defenses.armor_group_generator import generate_armor_group
from gen.fleet.ship_group_generator import (
generate_carrier_group,
@ -142,12 +143,12 @@ class LocationFinder:
self.miz_data = MizDataLocationFinder.compute_possible_locations(
game.theater.terrain.name, control_point.full_name)
def location_for(self, location_type: LocationType) -> Optional[Point]:
def location_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
position = self.control_point.preset_locations.random_for(location_type)
if position is not None:
return position
logging.warning(f"No campaign location for %s at %s",
logging.warning(f"No campaign location for %s Mat %s",
location_type.value, self.control_point)
position = self.random_from_miz_data(
location_type == LocationType.OffshoreStrikeTarget)
@ -164,7 +165,7 @@ class LocationFinder:
location_type.value, self.control_point)
return None
def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
def random_from_miz_data(self, offshore: bool) -> Optional[PointWithHeading]:
if offshore:
locations = self.miz_data.offshore_locations
else:
@ -172,11 +173,16 @@ class LocationFinder:
if self.miz_data.offshore_locations:
preset = random.choice(locations)
locations.remove(preset)
return preset.position
return PointWithHeading.from_point(preset.position, preset.heading)
return None
def random_position(self, location_type: LocationType) -> Optional[Point]:
def random_position(self, location_type: LocationType) -> Optional[PointWithHeading]:
# TODO: Flesh out preset locations so we never hit this case.
if location_type == LocationType.Coastal:
# No coastal locations generated randomly
return None
logging.warning("Falling back to random location for %s at %s",
location_type.value, self.control_point)
@ -228,7 +234,7 @@ class LocationFinder:
def _find_random_position(self, min_range: int, max_range: int,
on_ground: bool, is_base_defense: bool,
avoid_others: bool) -> Optional[Point]:
avoid_others: bool) -> Optional[PointWithHeading]:
"""
Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on
@ -241,7 +247,7 @@ class LocationFinder:
near = self.control_point.position
others = self.control_point.ground_objects
def is_valid(point: Optional[Point]) -> bool:
def is_valid(point: Optional[PointWithHeading]) -> bool:
if point is None:
return False
@ -272,9 +278,9 @@ class LocationFinder:
for _ in range(300):
# Check if on land or sea
p = near.random_point_within(max_range, min_range)
p = PointWithHeading.from_point(near.random_point_within(max_range, min_range), random.randint(0, 360))
if is_valid(p):
return p
return
return None
@ -544,6 +550,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
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:
@ -668,6 +677,26 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
return
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
group_id = self.game.next_group_id()
g = CoastalSiteGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, position.heading)
group = generate_coastal_group(self.game, g, self.faction_name)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
return
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate(self) -> bool:

View File

@ -309,6 +309,23 @@ class MissileSiteGroundObject(TheaterGroundObject):
)
class CoastalSiteGroundObject(TheaterGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, heading) -> None:
super().__init__(
name=name,
category="aa",
group_id=group_id,
position=position,
heading=heading,
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=False
)
class BaseDefenseGroundObject(TheaterGroundObject):
"""Base type for all base defenses."""

View File

@ -0,0 +1,27 @@
import logging
import random
from game import db
from gen.coastal.silkworm import SilkwormGenerator
COASTAL_MAP = {
"SilkwormGenerator": SilkwormGenerator,
}
def generate_coastal_group(game, ground_object, faction_name: str):
"""
This generate a coastal defenses group
:return: Nothing, but put the group reference inside the ground object
"""
faction = db.FACTIONS[faction_name]
if len(faction.coastal_defenses) > 0:
generators = faction.coastal_defenses
if len(generators) > 0:
gen = random.choice(generators)
if gen in COASTAL_MAP.keys():
generator = COASTAL_MAP[gen](game, ground_object, faction)
generator.generate()
return generator.get_generated_group()
else:
logging.info("Unable to generate missile group, generator : " + str(gen) + "does not exists")
return None

32
gen/coastal/silkworm.py Normal file
View File

@ -0,0 +1,32 @@
from dcs.vehicles import MissilesSS, Unarmed, AirDefence
from gen.sam.group_generator import GroupGenerator
class SilkwormGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(SilkwormGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
positions = self.get_circular_position(3, launcher_distance=120, coverage=180)
self.add_unit(MissilesSS.Silkworm_Radar, "SR#0", self.position.x, self.position.y, self.heading)
# Launchers
for i, p in enumerate(positions):
self.add_unit(MissilesSS.SS_N_2_Silkworm, "Missile#" + str(i), p[0], p[1], self.heading)
# Commander
self.add_unit(Unarmed.Transport_UAZ_469, "UAZ#0", self.position.x - 35, self.position.y - 20,
self.heading)
# Shorad
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38,
self.heading)
# Shorad 2
self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "STRELA#0",
self.position.x + 200, self.position.y + 15, 90)

View File

@ -39,6 +39,7 @@ def generate_ship_group(game, ground_object, faction_name: str):
gen = random.choice(faction.navy_generators)
if gen in SHIP_MAP.keys():
generator = SHIP_MAP[gen](game, ground_object, faction)
print(generator.position)
generator.generate()
return generator.get_generated_group()
else:

View File

@ -12,7 +12,7 @@ MISSILES_MAP = {
def generate_missile_group(game, ground_object, faction_name: str):
"""
This generate a ship group
This generate a missiles group
:return: Nothing, but put the group reference inside the ground object
"""
faction = db.FACTIONS[faction_name]

View File

@ -134,6 +134,8 @@ def load_icons():
ICONS["missile_blue"] = QPixmap("./resources/ui/ground_assets/missile_blue.png")
ICONS["nothreat"] = QPixmap("./resources/ui/ground_assets/nothreat.png")
ICONS["nothreat_blue"] = QPixmap("./resources/ui/ground_assets/nothreat_blue.png")
ICONS["coastal"] = QPixmap("./resources/ui/ground_assets/coastal.png")
ICONS["coastal_blue"] = QPixmap("./resources/ui/ground_assets/coastal_blue.png")
ICONS["Generator"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/generator.png")
ICONS["Missile"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/missile.png")

View File

@ -9,7 +9,7 @@ from game import Game
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import REWARDS
from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import MissileSiteGroundObject
from game.theater.theatergroundobject import MissileSiteGroundObject, CoastalSiteGroundObject
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions
@ -77,6 +77,8 @@ class QMapGroundObject(QMapObject):
cat = "ship"
if isinstance(self.ground_object, MissileSiteGroundObject):
cat = "missile"
if isinstance(self.ground_object, CoastalSiteGroundObject):
cat = "coastal"
rect = QRect(option.rect.x() + 2, option.rect.y(),
option.rect.width() - 2, option.rect.height())

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -57,6 +57,10 @@
"BoxSpringGenerator",
"TallRackGenerator"
],
"coastal_defenses": [
"SilkwormGenerator"
],
"coastal_group_count": 2,
"aircraft_carrier": [
"CV_1143_5_Admiral_Kuznetsov"
],

View File

@ -69,6 +69,10 @@
"ScudGenerator"
],
"missiles_group_count": 1,
"coastal_defenses": [
"SilkwormGenerator"
],
"coastal_group_count": 2,
"navy_generators": [
"GrishaGroupGenerator",
"MolniyaGroupGenerator"

View File

@ -79,6 +79,10 @@
"ScudGenerator"
],
"missiles_group_count": 1,
"coastal_defenses": [
"SilkwormGenerator"
],
"coastal_group_count": 3,
"navy_generators": [
"GrishaGroupGenerator",
"MolniyaGroupGenerator"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB