def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None: # Special Handling for scenery Objects based on trigger zones g = BuildingGroundObject( namegen.random_objective_name(), scenery.category, scenery.position, Heading.from_degrees(0), self.control_point, ) ground_group = TheaterGroup( self.game.next_group_id(), scenery.zone_def.name, PointWithHeading.from_point(scenery.position, Heading.from_degrees(0)), [], g, ) g.groups.append(ground_group) # Each nested trigger zone is a target/building/unit for an objective. for zone in scenery.zones: scenery_unit = SceneryUnit( zone.id, zone.name, dcs.statics.Fortification.White_Flag, PointWithHeading.from_point(zone.position, Heading.from_degrees(0)), g, ) scenery_unit.zone = zone ground_group.units.append(scenery_unit) self.control_point.connected_objectives.append(g)
def _generate_group( self, side: Country, unit_type: GroundUnitType, count: int, at: Point, move_formation: PointAction = PointAction.OffRoad, heading: Heading = Heading.from_degrees(0), ) -> VehicleGroup: if side == self.conflict.attackers_country: cp = self.conflict.blue_cp else: cp = self.conflict.red_cp group = self.mission.vehicle_group( side, namegen.next_unit_name(side, unit_type), unit_type.dcs_unit_type, position=at, group_size=count, heading=heading.degrees, move_formation=move_formation, ) self.unit_map.add_front_line_units(group, cp, unit_type) for c in range(count): vehicle: Vehicle = group.units[c] vehicle.player_can_drive = True return group
def for_pydcs_airport(cls, theater: ConflictTheater, airport: Airport) -> Iterator[RunwayData]: for runway in airport.runways: runway_number = runway.heading // 10 runway_side = ["", "L", "R"][runway.leftright] runway_name = f"{runway_number:02}{runway_side}" yield cls.for_airfield(theater, airport, Heading.from_degrees(runway.heading), runway_name) # pydcs only exposes one runway per physical runway, so to expose # both sides of the runway we need to generate the other. heading = Heading.from_degrees(runway.heading).opposite runway_number = heading.degrees // 10 runway_side = ["", "R", "L"][runway.leftright] runway_name = f"{runway_number:02}{runway_side}" yield cls.for_airfield(theater, airport, heading, runway_name)
def random_wind(minimum: int, maximum: int) -> WindConditions: wind_direction = Heading.random() wind_direction_2000m = wind_direction + Heading.random(-90, 90) wind_direction_8000m = wind_direction + Heading.random(-90, 90) at_0m_factor = 1 at_2000m_factor = 2 at_8000m_factor = 3 base_wind = random.randint(minimum, maximum) return WindConditions( # Always some wind to make the smoke move a bit. at_0m=Wind(wind_direction.degrees, max(1, base_wind * at_0m_factor)), at_2000m=Wind(wind_direction_2000m.degrees, base_wind * at_2000m_factor), at_8000m=Wind(wind_direction_8000m.degrees, base_wind * at_8000m_factor), )
def active_runway( self, theater: ConflictTheater, conditions: Conditions, dynamic_runways: Dict[str, RunwayData], ) -> RunwayData: logging.warning("TODO: FOBs have no runways.") return RunwayData( self.full_name, runway_heading=Heading.from_degrees(0), runway_name="" )
def from_template(id: int, dcs_type: Type[DcsUnitType], t: LayoutUnit, go: TheaterGroundObject) -> TheaterUnit: return TheaterUnit( id, t.name, dcs_type, PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)), go, )
def active_runway( self, theater: ConflictTheater, conditions: Conditions, dynamic_runways: Dict[str, RunwayData], ) -> RunwayData: # TODO: Assign TACAN and ICLS earlier so we don't need this. fallback = RunwayData( self.full_name, runway_heading=Heading.from_degrees(0), runway_name="" ) return dynamic_runways.get(self.name, fallback)
def build(self) -> PatrollingLayout: racetrack_half_distance = nautical_miles(20).meters location = self.package.target closest_boundary = self.threat_zones.closest_boundary( location.position) heading_to_threat_boundary = Heading.from_degrees( location.position.heading_between_point(closest_boundary)) distance_to_threat = meters( location.position.distance_to_point(closest_boundary)) orbit_heading = heading_to_threat_boundary # Station 70nm outside the threat zone. threat_buffer = nautical_miles(70) if self.threat_zones.threatened(location.position): orbit_distance = distance_to_threat + threat_buffer else: orbit_distance = distance_to_threat - threat_buffer racetrack_center = location.position.point_from_heading( orbit_heading.degrees, orbit_distance.meters) racetrack_start = racetrack_center.point_from_heading( orbit_heading.right.degrees, racetrack_half_distance) racetrack_end = racetrack_center.point_from_heading( orbit_heading.left.degrees, racetrack_half_distance) builder = WaypointBuilder(self.flight, self.coalition) tanker_type = self.flight.unit_type if tanker_type.patrol_altitude is not None: altitude = tanker_type.patrol_altitude else: altitude = feet(21000) racetrack = builder.race_track(racetrack_start, racetrack_end, altitude) return PatrollingLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path(self.flight.departure.position, racetrack_start, altitude), nav_from=builder.nav_path(racetrack_end, self.flight.arrival.position, altitude), patrol_start=racetrack[0], patrol_end=racetrack[1], arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), )
def build(self) -> PatrollingLayout: package_waypoints = self.package.waypoints assert package_waypoints is not None racetrack_half_distance = Distance.from_nautical_miles(20).meters racetrack_center = package_waypoints.refuel split_heading = Heading.from_degrees( racetrack_center.heading_between_point(package_waypoints.split)) home_heading = split_heading.opposite racetrack_start = racetrack_center.point_from_heading( split_heading.degrees, racetrack_half_distance) racetrack_end = racetrack_center.point_from_heading( home_heading.degrees, racetrack_half_distance) builder = WaypointBuilder(self.flight, self.coalition) tanker_type = self.flight.unit_type if tanker_type.patrol_altitude is not None: altitude = tanker_type.patrol_altitude else: altitude = feet(21000) racetrack = builder.race_track(racetrack_start, racetrack_end, altitude) return PatrollingLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path(self.flight.departure.position, racetrack_start, altitude), nav_from=builder.nav_path(racetrack_end, self.flight.arrival.position, altitude), patrol_start=racetrack[0], patrol_end=racetrack[1], arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), )
def build(self) -> SweepLayout: assert self.package.waypoints is not None target = self.package.target.position heading = Heading.from_degrees( self.package.waypoints.join.heading_between_point(target) ) start_pos = target.point_from_heading( heading.degrees, -self.doctrine.sweep_distance.meters ) builder = WaypointBuilder(self.flight, self.coalition) start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude) hold = builder.hold(self._hold_point()) refuel = None if self.package.waypoints is not None: refuel = builder.refuel(self.package.waypoints.refuel) return SweepLayout( departure=builder.takeoff(self.flight.departure), hold=hold, nav_to=builder.nav_path( hold.position, start.position, self.doctrine.ingress_altitude ), nav_from=builder.nav_path( end.position, self.flight.arrival.position, self.doctrine.ingress_altitude, ), sweep_start=start, sweep_end=end, refuel=refuel, arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), )
def add_preset_locations(self) -> None: for static in self.offshore_strike_targets: closest, distance = self.objective_info(static) closest.preset_locations.offshore_strike_locations.append( PointWithHeading.from_point( static.position, Heading.from_degrees(static.units[0].heading) ) ) for ship in self.ships: closest, distance = self.objective_info(ship, allow_naval=True) closest.preset_locations.ships.append( PointWithHeading.from_point( ship.position, Heading.from_degrees(ship.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, Heading.from_degrees(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, Heading.from_degrees(group.units[0].heading) ) ) for group in self.long_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.long_range_sams.append( PointWithHeading.from_point( group.position, Heading.from_degrees(group.units[0].heading) ) ) for group in self.medium_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.medium_range_sams.append( PointWithHeading.from_point( group.position, Heading.from_degrees(group.units[0].heading) ) ) for group in self.short_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.short_range_sams.append( PointWithHeading.from_point( group.position, Heading.from_degrees(group.units[0].heading) ) ) for group in self.aaa: closest, distance = self.objective_info(group) closest.preset_locations.aaa.append( PointWithHeading.from_point( group.position, Heading.from_degrees(group.units[0].heading) ) ) for group in self.ewrs: closest, distance = self.objective_info(group) closest.preset_locations.ewrs.append( PointWithHeading.from_point( group.position, Heading.from_degrees(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, Heading.from_degrees(group.units[0].heading) ) ) for static in self.helipads: closest, distance = self.objective_info(static) closest.helipads.append( PointWithHeading.from_point( static.position, Heading.from_degrees(static.units[0].heading) ) ) for static in self.factories: closest, distance = self.objective_info(static) closest.preset_locations.factories.append( PointWithHeading.from_point( static.position, Heading.from_degrees(static.units[0].heading) ) ) for static in self.ammunition_depots: closest, distance = self.objective_info(static) closest.preset_locations.ammunition_depots.append( PointWithHeading.from_point( static.position, Heading.from_degrees(static.units[0].heading) ) ) for static in self.strike_targets: closest, distance = self.objective_info(static) closest.preset_locations.strike_locations.append( PointWithHeading.from_point( static.position, Heading.from_degrees(static.units[0].heading) ) ) for scenery_group in self.scenery: closest, distance = self.objective_info(scenery_group) closest.preset_locations.scenery.append(scenery_group)
def heading(self) -> Heading: return Heading.from_degrees(self.airport.runways[0].heading)
def angle_off_headwind(self, runway: RunwayData) -> Heading: wind = Heading.from_degrees( self.conditions.weather.wind.at_0m.direction) ideal_heading = wind.opposite return runway.runway_heading.angle_between(ideal_heading)
def cap_racetrack_for_objective(self, location: MissionTarget, barcap: bool) -> tuple[Point, Point]: closest_cache = ObjectiveDistanceCache.get_closest_airfields(location) for airfield in closest_cache.operational_airfields: # If the mission is a BARCAP of an enemy airfield, find the *next* # closest enemy airfield. if airfield == self.package.target: continue if airfield.captured != self.is_player: closest_airfield = airfield break else: raise PlanningError("Could not find any enemy airfields") heading = Heading.from_degrees( location.position.heading_between_point(closest_airfield.position)) position = ShapelyPoint(self.package.target.position.x, self.package.target.position.y) if barcap: # BARCAPs should remain far enough back from the enemy that their # commit range does not enter the enemy's threat zone. Include a 5nm # buffer. distance_to_no_fly = ( meters(position.distance(self.threat_zones.all)) - self.doctrine.cap_engagement_range - nautical_miles(5)) max_track_length = self.doctrine.cap_max_track_length else: # Other race tracks (TARCAPs, currently) just try to keep some # distance from the nearest enemy airbase, but since they are by # definition in enemy territory they can't avoid the threat zone # without being useless. min_distance_from_enemy = nautical_miles(20) distance_to_airfield = meters( closest_airfield.position.distance_to_point( self.package.target.position)) distance_to_no_fly = distance_to_airfield - min_distance_from_enemy # TARCAPs fly short racetracks because they need to react faster. max_track_length = self.doctrine.cap_min_track_length + 0.3 * ( self.doctrine.cap_max_track_length - self.doctrine.cap_min_track_length) min_cap_distance = min(self.doctrine.cap_min_distance_from_cp, distance_to_no_fly) max_cap_distance = min(self.doctrine.cap_max_distance_from_cp, distance_to_no_fly) end = location.position.point_from_heading( heading.degrees, random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)), ) track_length = random.randint( int(self.doctrine.cap_min_track_length.meters), int(max_track_length.meters), ) start = end.point_from_heading(heading.opposite.degrees, track_length) return start, end
def heading(self) -> Heading: return Heading.from_degrees(0)
def generate(self) -> None: player_cp = (self.conflict.blue_cp if self.conflict.blue_cp.captured else self.conflict.red_cp) country = self.mission.country(self.game.blue.country_name) if not self.game.settings.disable_legacy_tanker: fallback_tanker_number = 0 for i, tanker_unit_type in enumerate( self.game.faction_for(player=True).tankers): unit_type = tanker_unit_type.dcs_unit_type if not issubclass(unit_type, PlaneType): logging.warning( f"Refueling aircraft {unit_type} must be a plane") continue # TODO: Make loiter altitude a property of the unit type. alt, airspeed = self._get_tanker_params( tanker_unit_type.dcs_unit_type) freq = self.radio_registry.alloc_uhf() tacan = self.tacan_registry.alloc_for_band( TacanBand.Y, TacanUsage.AirToAir) tanker_heading = Heading.from_degrees( self.conflict.red_cp.position.heading_between_point( self.conflict.blue_cp.position) + TANKER_HEADING_OFFSET * i) tanker_position = player_cp.position.point_from_heading( tanker_heading.degrees, TANKER_DISTANCE) tanker_group = self.mission.refuel_flight( country=country, name=namegen.next_tanker_name(country, tanker_unit_type), airport=None, plane_type=unit_type, position=tanker_position, altitude=alt, race_distance=58000, frequency=freq.mhz, start_type=StartType.Warm, speed=airspeed, tacanchannel=str(tacan), ) tanker_group.set_frequency(freq.mhz) callsign = callsign_for_support_unit(tanker_group) tacan_callsign = { "Texaco": "TEX", "Arco": "ARC", "Shell": "SHL", }.get(callsign) if tacan_callsign is None: # The dict above is all the callsigns currently in the game, but # non-Western countries don't use the callsigns and instead just # use numbers. It's possible that none of those nations have # TACAN compatible refueling aircraft, but fallback just in # case. tacan_callsign = f"TK{fallback_tanker_number}" fallback_tanker_number += 1 if tanker_unit_type != IL_78M: # Override PyDCS tacan channel. tanker_group.points[0].tasks.pop() tanker_group.points[0].tasks.append( ActivateBeaconCommand( tacan.number, tacan.band.value, tacan_callsign, True, tanker_group.units[0].id, True, )) tanker_group.points[0].tasks.append(SetInvisibleCommand(True)) tanker_group.points[0].tasks.append(SetImmortalCommand(True)) self.air_support.tankers.append( TankerInfo( str(tanker_group.name), callsign, tanker_unit_type.name, freq, tacan, start_time=None, end_time=None, blue=True, )) if not self.game.settings.disable_legacy_aewc: possible_awacs = [ a for a in self.game.faction_for(player=True).aircrafts if a in AEWC_CAPABLE ] if not possible_awacs: logging.warning("No AWACS for faction") return awacs_unit = possible_awacs[0] freq = self.radio_registry.alloc_uhf() unit_type = awacs_unit.dcs_unit_type if not issubclass(unit_type, PlaneType): logging.warning(f"AWACS aircraft {unit_type} must be a plane") return awacs_flight = self.mission.awacs_flight( country=country, name=namegen.next_awacs_name(country), plane_type=unit_type, altitude=AWACS_ALT, airport=None, position=self.conflict.position.random_point_within( AWACS_DISTANCE, AWACS_DISTANCE), frequency=freq.mhz, start_type=StartType.Warm, ) awacs_flight.set_frequency(freq.mhz) awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) self.air_support.awacs.append( AwacsInfo( group_name=str(awacs_flight.name), callsign=callsign_for_support_unit(awacs_flight), freq=freq, depature_location=None, start_time=None, end_time=None, blue=True, ))
def _plan_tank_ifv_action( self, stance: CombatStance, enemy_groups: List[Tuple[VehicleGroup, CombatGroup]], dcs_group: VehicleGroup, forward_heading: Heading, to_cp: ControlPoint, ) -> bool: """ Handles adding the DCS tasks for tank and IFV groups for all combat stances. Returns True if tasking was added, returns False if the stance was not a combat stance. """ self._set_reform_waypoint(dcs_group, forward_heading) if stance == CombatStance.AGGRESSIVE: # Attack nearest enemy if any # Then move forward OR Attack enemy base if it is not too far away target = self.find_nearest_enemy_group(dcs_group, enemy_groups) if target is not None: rand_offset = Vector2( random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), ) target_point = self.conflict.theater.nearest_land_pos( target.points[0].position + rand_offset) dcs_group.add_waypoint(target_point) dcs_group.points[2].tasks.append(AttackGroup(target.id)) if (to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE): attack_point = self.conflict.theater.nearest_land_pos( to_cp.position.random_point_within(500, 0)) else: # We use an offset heading here because DCS doesn't always # force vehicles to move if there's no heading change. offset_heading = forward_heading - Heading.from_degrees(2) attack_point = self.find_offensive_point( dcs_group, offset_heading, AGGRESIVE_MOVE_DISTANCE) dcs_group.add_waypoint(attack_point, PointAction.OffRoad) elif stance == CombatStance.BREAKTHROUGH: # In breakthrough mode, the units will move forward # If the enemy base is close enough, the units will attack the base if (to_cp.position.distance_to_point(dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE): attack_point = self.conflict.theater.nearest_land_pos( to_cp.position.random_point_within(500, 0)) else: # We use an offset heading here because DCS doesn't always # force vehicles to move if there's no heading change. offset_heading = forward_heading - Heading.from_degrees(1) attack_point = self.find_offensive_point( dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE) dcs_group.add_waypoint(attack_point, PointAction.OffRoad) elif stance == CombatStance.ELIMINATION: # In elimination mode, the units focus on destroying as much enemy groups as possible targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3) for i, target in enumerate(targets, start=1): rand_offset = Vector2( random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), ) target_point = self.conflict.theater.nearest_land_pos( target.points[0].position + rand_offset) dcs_group.add_waypoint(target_point, PointAction.OffRoad) dcs_group.points[i + 1].tasks.append(AttackGroup(target.id)) if (to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE): attack_point = self.conflict.theater.nearest_land_pos( to_cp.position.random_point_within(500, 0)) dcs_group.add_waypoint(attack_point) if stance != CombatStance.RETREAT: self.add_morale_trigger(dcs_group, forward_heading) return True return False
def heading(self) -> Heading: return Heading.from_degrees(0) # TODO compute heading
def _plan_artillery_action( self, stance: CombatStance, gen_group: CombatGroup, dcs_group: VehicleGroup, forward_heading: Heading, target: Point, ) -> bool: """ Handles adding the DCS tasks for artillery groups for all combat stances. Returns True if tasking was added, returns False if the stance was not a combat stance. """ self._set_reform_waypoint(dcs_group, forward_heading) if stance != CombatStance.RETREAT: hold_task = Hold() hold_task.number = 1 dcs_group.add_trigger_action(hold_task) # Artillery strike random start artillery_trigger = TriggerOnce( Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id)) artillery_trigger.add_condition( TimeAfter(seconds=random.randint(1, 45) * 60)) # TODO: Update to fire at group instead of point fire_task = FireAtPoint(target, gen_group.size * 10, 100) fire_task.number = 2 if stance != CombatStance.RETREAT else 1 dcs_group.add_trigger_action(fire_task) artillery_trigger.add_action( AITaskPush(dcs_group.id, len(dcs_group.tasks))) self.mission.triggerrules.triggers.append(artillery_trigger) # Artillery will fall back when under attack if stance != CombatStance.RETREAT: # Hold position dcs_group.points[1].tasks.append(Hold()) retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 3)) dcs_group.add_waypoint( dcs_group.position.point_from_heading(forward_heading.degrees, 1), PointAction.OffRoad, ) dcs_group.points[2].tasks.append(Hold()) dcs_group.add_waypoint(retreat, PointAction.OffRoad) artillery_fallback = TriggerOnce( Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id)) for i, u in enumerate(dcs_group.units): artillery_fallback.add_condition(UnitDamaged(u.id)) if i < len(dcs_group.units) - 1: artillery_fallback.add_condition(Or()) hold_2 = Hold() hold_2.number = 3 dcs_group.add_trigger_action(hold_2) retreat_task = GoToWaypoint(to_index=3) retreat_task.number = 4 dcs_group.add_trigger_action(retreat_task) artillery_fallback.add_action( AITaskPush(dcs_group.id, len(dcs_group.tasks))) self.mission.triggerrules.triggers.append(artillery_fallback) for u in dcs_group.units: u.heading = (forward_heading + Heading.random(-5, 5)).degrees return True return False