diff --git a/game/factions/faction.py b/game/factions/faction.py index d3b14134..0c6371f0 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -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") diff --git a/game/point_with_heading.py b/game/point_with_heading.py new file mode 100644 index 00000000..06a844b1 --- /dev/null +++ b/game/point_with_heading.py @@ -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 \ No newline at end of file diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 419f216e..673a7c36 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -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: diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 1ca122af..e406e1ac 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -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: diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 118861d1..d9eacefa 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -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: diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 701f8c9a..2a660cf3 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -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.""" diff --git a/gen/coastal/coastal_group_generator.py b/gen/coastal/coastal_group_generator.py new file mode 100644 index 00000000..63ac4a03 --- /dev/null +++ b/gen/coastal/coastal_group_generator.py @@ -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 \ No newline at end of file diff --git a/gen/coastal/silkworm.py b/gen/coastal/silkworm.py new file mode 100644 index 00000000..6bf773ea --- /dev/null +++ b/gen/coastal/silkworm.py @@ -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) \ No newline at end of file diff --git a/gen/fleet/ship_group_generator.py b/gen/fleet/ship_group_generator.py index db0a78cd..ad5bf457 100644 --- a/gen/fleet/ship_group_generator.py +++ b/gen/fleet/ship_group_generator.py @@ -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: diff --git a/gen/missiles/missiles_group_generator.py b/gen/missiles/missiles_group_generator.py index 3b84037d..0e2ab2ec 100644 --- a/gen/missiles/missiles_group_generator.py +++ b/gen/missiles/missiles_group_generator.py @@ -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] diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index 1ac196f6..04320a7d 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -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") diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py index da69a9fc..22f41bdc 100644 --- a/qt_ui/widgets/map/QMapGroundObject.py +++ b/qt_ui/widgets/map/QMapGroundObject.py @@ -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()) diff --git a/resources/campaigns/black_sea.miz b/resources/campaigns/black_sea.miz index 287ea1cb..c77e79ad 100644 Binary files a/resources/campaigns/black_sea.miz and b/resources/campaigns/black_sea.miz differ diff --git a/resources/campaigns/black_sea_lite.miz b/resources/campaigns/black_sea_lite.miz index 4c6297ab..71a6a285 100644 Binary files a/resources/campaigns/black_sea_lite.miz and b/resources/campaigns/black_sea_lite.miz differ diff --git a/resources/campaigns/desert_war.miz b/resources/campaigns/desert_war.miz index a1044bac..fedbf10a 100644 Binary files a/resources/campaigns/desert_war.miz and b/resources/campaigns/desert_war.miz differ diff --git a/resources/campaigns/emirates.miz b/resources/campaigns/emirates.miz index 949e6575..2dba325a 100644 Binary files a/resources/campaigns/emirates.miz and b/resources/campaigns/emirates.miz differ diff --git a/resources/campaigns/invasion_of_iran.miz b/resources/campaigns/invasion_of_iran.miz index ac7ed7f6..de62a998 100644 Binary files a/resources/campaigns/invasion_of_iran.miz and b/resources/campaigns/invasion_of_iran.miz differ diff --git a/resources/campaigns/invasion_of_iran_lite.miz b/resources/campaigns/invasion_of_iran_lite.miz index 5c0b2aad..7791cc97 100644 Binary files a/resources/campaigns/invasion_of_iran_lite.miz and b/resources/campaigns/invasion_of_iran_lite.miz differ diff --git a/resources/factions/china_2010.json b/resources/factions/china_2010.json index cc592f1d..2320a4ff 100644 --- a/resources/factions/china_2010.json +++ b/resources/factions/china_2010.json @@ -57,6 +57,10 @@ "BoxSpringGenerator", "TallRackGenerator" ], + "coastal_defenses": [ + "SilkwormGenerator" + ], + "coastal_group_count": 2, "aircraft_carrier": [ "CV_1143_5_Admiral_Kuznetsov" ], diff --git a/resources/factions/iran_1988.json b/resources/factions/iran_1988.json index 406bd9ab..5792d311 100644 --- a/resources/factions/iran_1988.json +++ b/resources/factions/iran_1988.json @@ -69,6 +69,10 @@ "ScudGenerator" ], "missiles_group_count": 1, + "coastal_defenses": [ + "SilkwormGenerator" + ], + "coastal_group_count": 2, "navy_generators": [ "GrishaGroupGenerator", "MolniyaGroupGenerator" diff --git a/resources/factions/iran_2015.json b/resources/factions/iran_2015.json index 9f3d8870..725c1069 100644 --- a/resources/factions/iran_2015.json +++ b/resources/factions/iran_2015.json @@ -79,6 +79,10 @@ "ScudGenerator" ], "missiles_group_count": 1, + "coastal_defenses": [ + "SilkwormGenerator" + ], + "coastal_group_count": 3, "navy_generators": [ "GrishaGroupGenerator", "MolniyaGroupGenerator" diff --git a/resources/ui/ground_assets/coastal.png b/resources/ui/ground_assets/coastal.png new file mode 100644 index 00000000..6a6740c8 Binary files /dev/null and b/resources/ui/ground_assets/coastal.png differ diff --git a/resources/ui/ground_assets/coastal_blue.png b/resources/ui/ground_assets/coastal_blue.png new file mode 100644 index 00000000..5c9e16b1 Binary files /dev/null and b/resources/ui/ground_assets/coastal_blue.png differ