diff --git a/.gitignore b/.gitignore index 197ba2a3..ccb1e474 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,6 @@ env/ /logs/ /resources/logging.yaml +/resources/plugins/pretense/pretense_output.lua *.psd diff --git a/game/ato/flightplans/flightplanbuildertypes.py b/game/ato/flightplans/flightplanbuildertypes.py index 9dd818f1..5ec691d2 100644 --- a/game/ato/flightplans/flightplanbuildertypes.py +++ b/game/ato/flightplans/flightplanbuildertypes.py @@ -19,6 +19,7 @@ from .ocaaircraft import OcaAircraftFlightPlan from .ocarunway import OcaRunwayFlightPlan from .packagerefueling import PackageRefuelingFlightPlan from .planningerror import PlanningError +from .pretensecargo import PretenseCargoFlightPlan from .sead import SeadFlightPlan from .seadsweep import SeadSweepFlightPlan from .strike import StrikeFlightPlan @@ -61,6 +62,7 @@ class FlightPlanBuilderTypes: FlightType.TRANSPORT: AirliftFlightPlan.builder_type(), FlightType.FERRY: FerryFlightPlan.builder_type(), FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(), + FlightType.PRETENSE_CARGO: PretenseCargoFlightPlan.builder_type(), FlightType.ARMED_RECON: ArmedReconFlightPlan.builder_type(), } try: diff --git a/game/ato/flightplans/pretensecargo.py b/game/ato/flightplans/pretensecargo.py new file mode 100644 index 00000000..f143aaa2 --- /dev/null +++ b/game/ato/flightplans/pretensecargo.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import random +from collections.abc import Iterator +from dataclasses import dataclass +from datetime import datetime +from typing import TYPE_CHECKING, Type + +from game.utils import feet +from .ferry import FerryLayout +from .ibuilder import IBuilder +from .planningerror import PlanningError +from .standard import StandardFlightPlan, StandardLayout +from .waypointbuilder import WaypointBuilder +from ...theater import OffMapSpawn + +if TYPE_CHECKING: + from ..flightwaypoint import FlightWaypoint + + +PRETENSE_CARGO_FLIGHT_DISTANCE = 100000 +PRETENSE_CARGO_FLIGHT_HEADING_RANGE = 20 + + +class PretenseCargoFlightPlan(StandardFlightPlan[FerryLayout]): + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + + @property + def tot_waypoint(self) -> FlightWaypoint: + return self.layout.arrival + + def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None: + # TOT planning isn't really useful for ferries. They're behind the front + # lines so no need to wait for escorts or for other missions to complete. + return None + + def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None: + return None + + @property + def mission_begin_on_station_time(self) -> datetime | None: + return None + + @property + def mission_departure_time(self) -> datetime: + return self.package.time_over_target + + +class Builder(IBuilder[PretenseCargoFlightPlan, FerryLayout]): + def layout(self) -> FerryLayout: + # Find the spawn location for off-map transport planes + distance_to_flot = 0.0 + heading_from_flot = 0.0 + offmap_transport_cp_id = self.flight.departure.id + for front_line_cp in self.coalition.game.theater.controlpoints: + if isinstance(front_line_cp, OffMapSpawn): + continue + for front_line in self.coalition.game.theater.conflicts(): + if front_line_cp.captured == self.flight.coalition.player: + if ( + front_line_cp.position.distance_to_point(front_line.position) + > distance_to_flot + ): + distance_to_flot = front_line_cp.position.distance_to_point( + front_line.position + ) + heading_from_flot = front_line.position.heading_between_point( + front_line_cp.position + ) + offmap_transport_cp_id = front_line_cp.id + offmap_transport_cp = self.coalition.game.theater.find_control_point_by_id( + offmap_transport_cp_id + ) + offmap_heading = random.randrange( + int(heading_from_flot - PRETENSE_CARGO_FLIGHT_HEADING_RANGE), + int(heading_from_flot + PRETENSE_CARGO_FLIGHT_HEADING_RANGE), + ) + offmap_transport_spawn = offmap_transport_cp.position.point_from_heading( + offmap_heading, PRETENSE_CARGO_FLIGHT_DISTANCE + ) + + altitude_is_agl = self.flight.is_helo + altitude = ( + feet(self.coalition.game.settings.heli_cruise_alt_agl) + if altitude_is_agl + else self.flight.unit_type.preferred_patrol_altitude + ) + + builder = WaypointBuilder(self.flight) + ferry_layout = FerryLayout( + departure=builder.join(offmap_transport_spawn), + nav_to=builder.nav_path( + offmap_transport_spawn, + self.flight.arrival.position, + altitude, + altitude_is_agl, + ), + arrival=builder.land(self.flight.arrival), + divert=builder.divert(self.flight.divert), + bullseye=builder.bullseye(), + nav_from=[], + custom_waypoints=list(), + ) + ferry_layout.departure = builder.join(offmap_transport_spawn) + ferry_layout.nav_to.append(builder.join(offmap_transport_spawn)) + ferry_layout.nav_from.append(builder.join(offmap_transport_spawn)) + return ferry_layout + + def build(self, dump_debug_info: bool = False) -> PretenseCargoFlightPlan: + return PretenseCargoFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flighttype.py b/game/ato/flighttype.py index c7b7e5e3..7b210e77 100644 --- a/game/ato/flighttype.py +++ b/game/ato/flighttype.py @@ -58,6 +58,7 @@ class FlightType(Enum): FERRY = "Ferry" AIR_ASSAULT = "Air Assault" SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD + PRETENSE_CARGO = "Cargo Transport" # For Pretense campaign AI cargo planes ARMED_RECON = "Armed Recon" def __str__(self) -> str: @@ -124,5 +125,6 @@ class FlightType(Enum): FlightType.SWEEP: AirEntity.FIGHTER, FlightType.TARCAP: AirEntity.FIGHTER, FlightType.TRANSPORT: AirEntity.UTILITY, + FlightType.PRETENSE_CARGO: AirEntity.UTILITY, FlightType.AIR_ASSAULT: AirEntity.ROTARY_WING, }.get(self, AirEntity.UNSPECIFIED) diff --git a/game/campaignloader/squadrondefgenerator.py b/game/campaignloader/squadrondefgenerator.py index 2742519a..f5e38552 100644 --- a/game/campaignloader/squadrondefgenerator.py +++ b/game/campaignloader/squadrondefgenerator.py @@ -23,6 +23,8 @@ class SquadronDefGenerator: def generate_for_task( self, task: FlightType, control_point: ControlPoint ) -> Optional[SquadronDef]: + settings = control_point.coalition.game.settings + squadron_random_chance = settings.squadron_random_chance aircraft_choice: Optional[AircraftType] = None for aircraft in AircraftType.priority_list_for_task(task): if aircraft not in self.faction.all_aircrafts: @@ -30,9 +32,9 @@ class SquadronDefGenerator: if not control_point.can_operate(aircraft): continue aircraft_choice = aircraft - # 50/50 chance to keep looking for an aircraft that isn't as far up the + # squadron_random_chance percent chance to keep looking for an aircraft that isn't as far up the # priority list to maintain some unit variety. - if random.choice([True, False]): + if squadron_random_chance >= random.randint(1, 100): break if aircraft_choice is None: diff --git a/game/game.py b/game/game.py index 761403b9..43598f40 100644 --- a/game/game.py +++ b/game/game.py @@ -22,6 +22,7 @@ from game.models.game_stats import GameStats from game.plugins import LuaPluginManager from game.utils import Distance from . import naming, persistency +from .ato import Flight from .ato.flighttype import FlightType from .campaignloader import CampaignAirWingConfig from .coalition import Coalition @@ -148,6 +149,16 @@ class Game: self.blue.configure_default_air_wing(air_wing_config) self.red.configure_default_air_wing(air_wing_config) + # Side, control point, mission type + self.pretense_ground_supply: dict[int, dict[str, List[str]]] = {1: {}, 2: {}} + self.pretense_ground_assault: dict[int, dict[str, List[str]]] = {1: {}, 2: {}} + self.pretense_air: dict[int, dict[str, dict[FlightType, List[str]]]] = { + 1: {}, + 2: {}, + } + self.pretense_air_groups: dict[str, Flight] = {} + self.pretense_carrier_zones: List[str] = [] + self.on_load(game_still_initializing=True) def __setstate__(self, state: dict[str, Any]) -> None: diff --git a/game/missiongenerator/aircraft/flightgroupconfigurator.py b/game/missiongenerator/aircraft/flightgroupconfigurator.py index ec42092c..4e815d28 100644 --- a/game/missiongenerator/aircraft/flightgroupconfigurator.py +++ b/game/missiongenerator/aircraft/flightgroupconfigurator.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import random from datetime import datetime from typing import Any, Optional, TYPE_CHECKING @@ -19,7 +20,12 @@ from game.data.weapons import Pylon, WeaponType from game.missiongenerator.logisticsgenerator import LogisticsGenerator from game.missiongenerator.missiondata import MissionData, AwacsInfo, TankerInfo from game.radio.radios import RadioFrequency, RadioRegistry -from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage +from game.radio.tacan import ( + TacanBand, + TacanRegistry, + TacanUsage, + OutOfTacanChannelsError, +) from game.runways import RunwayData from game.squadrons import Pilot from .aircraftbehavior import AircraftBehavior @@ -210,9 +216,12 @@ class FlightGroupConfigurator: ) or isinstance(self.flight.flight_plan, PackageRefuelingFlightPlan): tacan = self.flight.tacan if tacan is None and self.flight.squadron.aircraft.dcs_unit_type.tacan: - tacan = self.tacan_registry.alloc_for_band( - TacanBand.Y, TacanUsage.AirToAir - ) + try: + tacan = self.tacan_registry.alloc_for_band( + TacanBand.Y, TacanUsage.AirToAir + ) + except OutOfTacanChannelsError: + tacan = random.choice(list(self.tacan_registry.allocated_channels)) else: tacan = self.flight.tacan self.mission_data.tankers.append( diff --git a/game/missiongenerator/aircraft/flightgroupspawner.py b/game/missiongenerator/aircraft/flightgroupspawner.py index 4bd9b75d..58d4e9b1 100644 --- a/game/missiongenerator/aircraft/flightgroupspawner.py +++ b/game/missiongenerator/aircraft/flightgroupspawner.py @@ -15,6 +15,7 @@ from dcs.planes import ( C_101CC, Su_33, MiG_15bis, + M_2000C, ) from dcs.point import PointAction from dcs.ships import KUZNECOW @@ -36,7 +37,7 @@ from game.missiongenerator.missiondata import MissionData from game.naming import namegen from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, OffMapSpawn from game.utils import feet, meters -from pydcs_extensions import A_4E_C +from pydcs_extensions import A_4E_C, VSN_F4B, VSN_F4C WARM_START_HELI_ALT = meters(500) WARM_START_ALTITUDE = meters(3000) @@ -496,7 +497,7 @@ class FlightGroupSpawner: ) -> Optional[FlyingGroup[Any]]: is_airbase = False is_roadbase = False - ground_spawn = None + ground_spawn: Optional[Tuple[StaticGroup, Point]] = None if not is_large and len(self.ground_spawns_roadbase[cp]) > 0: ground_spawn = self.ground_spawns_roadbase[cp].pop() @@ -519,6 +520,8 @@ class FlightGroupSpawner: group.points[0].type = "TakeOffGround" group.units[0].heading = ground_spawn[0].units[0].heading + self._remove_invisible_farps_if_requested(cp, ground_spawn[0], group) + # Hot start aircraft which require ground power to start, when ground power # trucks have been disabled for performance reasons ground_power_available = ( @@ -529,10 +532,31 @@ class FlightGroupSpawner: and self.flight.coalition.game.settings.ground_start_ground_power_trucks_roadbase ) - if self.start_type is not StartType.COLD or ( - not ground_power_available - and self.flight.unit_type.dcs_unit_type - in [A_4E_C, F_5E_3, F_86F_Sabre, MiG_15bis, F_14A_135_GR, F_14B, C_101CC] + # Also hot start aircraft which require ground crew support (ground air or chock removal) + # which might not be available at roadbases + if ( + self.start_type is not StartType.COLD + or ( + not ground_power_available + and self.flight.unit_type.dcs_unit_type + in [ + A_4E_C, + F_86F_Sabre, + MiG_15bis, + F_14A_135_GR, + F_14B, + C_101CC, + ] + ) + or ( + self.flight.unit_type.dcs_unit_type + in [ + F_5E_3, + M_2000C, + VSN_F4B, + VSN_F4C, + ] + ) ): group.points[0].action = PointAction.FromGroundAreaHot group.points[0].type = "TakeOffGroundHot" @@ -556,12 +580,33 @@ class FlightGroupSpawner: ground_spawn[0].x, ground_spawn[0].y, terrain=terrain ) group.units[1 + i].heading = ground_spawn[0].units[0].heading + + self._remove_invisible_farps_if_requested(cp, ground_spawn[0]) except IndexError as ex: - raise RuntimeError( - f"Not enough ground spawn slots available at {cp}" + raise NoParkingSlotError( + f"Not enough STOL slots available at {cp}" ) from ex return group + def _remove_invisible_farps_if_requested( + self, + cp: ControlPoint, + ground_spawn: StaticGroup, + group: Optional[FlyingGroup[Any]] = None, + ) -> None: + if ( + cp.coalition.game.settings.ground_start_airbase_statics_farps_remove + and isinstance(cp, Airfield) + ): + # Remove invisible FARPs from airfields because they are unnecessary + neutral_country = self.mission.country( + cp.coalition.game.neutral_country.name + ) + neutral_country.remove_static_group(ground_spawn) + if group: + group.points[0].link_unit = None + group.points[0].helipad_id = None + def dcs_start_type(self) -> DcsStartType: if self.start_type is StartType.RUNWAY: return DcsStartType.Runway diff --git a/game/missiongenerator/missiondata.py b/game/missiongenerator/missiondata.py index 8906feb4..062d6a56 100644 --- a/game/missiongenerator/missiondata.py +++ b/game/missiongenerator/missiondata.py @@ -52,6 +52,8 @@ class CarrierInfo(UnitInfo): """Carrier information.""" tacan: TacanChannel + icls_channel: int | None + link4_freq: RadioFrequency | None @dataclass diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py index 55a9b2df..73acdc64 100644 --- a/game/missiongenerator/tgogenerator.py +++ b/game/missiongenerator/tgogenerator.py @@ -67,6 +67,7 @@ from game.theater import ( TheaterGroundObject, TheaterUnit, NavalControlPoint, + Airfield, ) from game.theater.theatergroundobject import ( CarrierGroundObject, @@ -297,11 +298,9 @@ class GroundObjectGenerator: # All alive Ships ship_units.append(unit) if vehicle_units: - vg = self.create_vehicle_group(group.group_name, vehicle_units) - vg.hidden_on_mfd = self.ground_object.hide_on_mfd + self.create_vehicle_group(group.group_name, vehicle_units) if ship_units: - sg = self.create_ship_group(group.group_name, ship_units) - sg.hidden_on_mfd = self.ground_object.hide_on_mfd + self.create_ship_group(group.group_name, ship_units) def create_vehicle_group( self, group_name: str, units: list[TheaterUnit] @@ -333,6 +332,7 @@ class GroundObjectGenerator: self._register_theater_unit(unit, vehicle_group.units[-1]) if vehicle_group is None: raise RuntimeError(f"Error creating VehicleGroup for {group_name}") + vehicle_group.hidden_on_mfd = self.ground_object.hide_on_mfd return vehicle_group def create_ship_group( @@ -369,6 +369,7 @@ class GroundObjectGenerator: self._register_theater_unit(unit, ship_group.units[-1]) if ship_group is None: raise RuntimeError(f"Error creating ShipGroup for {group_name}") + ship_group.hidden_on_mfd = self.ground_object.hide_on_mfd return ship_group def create_static_group(self, unit: TheaterUnit) -> None: @@ -645,6 +646,8 @@ class GenericCarrierGenerator(GroundObjectGenerator): callsign=tacan_callsign, freq=atc, tacan=tacan, + icls_channel=icls, + link4_freq=link4, blue=self.control_point.captured, ) ) @@ -844,6 +847,20 @@ class HelipadGenerator: else: self.helipads.append(sg) + if self.game.position_culled(helipad): + cull_farp_statics = True + if self.cp.coalition.player: + for package in self.cp.coalition.ato.packages: + for flight in package.flights: + if flight.squadron.location == self.cp: + cull_farp_statics = False + break + elif flight.divert and flight.divert == self.cp: + cull_farp_statics = False + break + else: + cull_farp_statics = False + warehouse = Airport( pad.position, self.m.terrain, @@ -852,30 +869,31 @@ class HelipadGenerator: # configure dynamic spawn + hot start of DS, plus dynamic cargo? self.m.warehouses.warehouses[pad.id] = warehouse - # Generate a FARP Ammo and Fuel stack for each pad - self.m.static_group( - country=country, - name=(name + "_fuel"), - _type=Fortification.FARP_Fuel_Depot, - position=pad.position.point_from_heading(helipad.heading.degrees, 35), - heading=pad.heading + 180, - ) - self.m.static_group( - country=country, - name=(name + "_ammo"), - _type=Fortification.FARP_Ammo_Dump_Coating, - position=pad.position.point_from_heading( - helipad.heading.degrees, 35 - ).point_from_heading(helipad.heading.degrees + 90, 10), - heading=pad.heading + 90, - ) - self.m.static_group( - country=country, - name=(name + "_ws"), - _type=Fortification.Windsock, - position=helipad.point_from_heading(helipad.heading.degrees + 45, 35), - heading=pad.heading, - ) + if not cull_farp_statics: + # Generate a FARP Ammo and Fuel stack for each pad + self.m.static_group( + country=country, + name=(name + "_fuel"), + _type=Fortification.FARP_Fuel_Depot, + position=pad.position.point_from_heading(helipad.heading.degrees, 35), + heading=pad.heading + 180, + ) + self.m.static_group( + country=country, + name=(name + "_ammo"), + _type=Fortification.FARP_Ammo_Dump_Coating, + position=pad.position.point_from_heading( + helipad.heading.degrees, 35 + ).point_from_heading(helipad.heading.degrees + 90, 10), + heading=pad.heading + 90, + ) + self.m.static_group( + country=country, + name=(name + "_ws"), + _type=Fortification.Windsock, + position=helipad.point_from_heading(helipad.heading.degrees + 45, 35), + heading=pad.heading, + ) def append_helipad( self, @@ -952,61 +970,88 @@ class GroundSpawnRoadbaseGenerator: country.id ) - # Generate ammo truck/farp and fuel truck/stack for each pad - if self.game.settings.ground_start_trucks_roadbase: - self.m.vehicle_group( - country=country, - name=(name + "_fuel"), - _type=tanker_type, - position=pad.position.point_from_heading( - ground_spawn[0].heading.degrees + 90, 35 - ), - group_size=1, - heading=pad.heading + 315, - move_formation=PointAction.OffRoad, - ) - self.m.vehicle_group( - country=country, - name=(name + "_ammo"), - _type=ammo_truck_type, - position=pad.position.point_from_heading( - ground_spawn[0].heading.degrees + 90, 35 - ).point_from_heading(ground_spawn[0].heading.degrees + 180, 10), - group_size=1, - heading=pad.heading + 315, - move_formation=PointAction.OffRoad, - ) + if self.game.settings.ground_start_airbase_statics_farps_remove and isinstance( + self.cp, Airfield + ): + cull_farp_statics = True + elif self.game.position_culled(ground_spawn[0]): + cull_farp_statics = True + if self.cp.coalition.player: + for package in self.cp.coalition.ato.packages: + for flight in package.flights: + if flight.squadron.location == self.cp: + cull_farp_statics = False + break + elif flight.divert and flight.divert == self.cp: + cull_farp_statics = False + break else: - self.m.static_group( - country=country, - name=(name + "_fuel"), - _type=Fortification.FARP_Fuel_Depot, - position=pad.position.point_from_heading( - ground_spawn[0].heading.degrees + 90, 35 - ), - heading=pad.heading + 270, - ) - self.m.static_group( - country=country, - name=(name + "_ammo"), - _type=Fortification.FARP_Ammo_Dump_Coating, - position=pad.position.point_from_heading( - ground_spawn[0].heading.degrees + 90, 35 - ).point_from_heading(ground_spawn[0].heading.degrees + 180, 10), - heading=pad.heading + 180, - ) - if self.game.settings.ground_start_ground_power_trucks_roadbase: - self.m.vehicle_group( - country=country, - name=(name + "_power"), - _type=power_truck_type, - position=pad.position.point_from_heading( - ground_spawn[0].heading.degrees + 90, 35 - ).point_from_heading(ground_spawn[0].heading.degrees + 180, 20), - group_size=1, - heading=pad.heading + 315, - move_formation=PointAction.OffRoad, - ) + cull_farp_statics = False + + warehouse = Airport( + pad.position, + self.m.terrain, + ).dict() + warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" + # configure dynamic spawn + hot start of DS, plus dynamic cargo? + self.m.warehouses.warehouses[pad.id] = warehouse + + if not cull_farp_statics: + # Generate ammo truck/farp and fuel truck/stack for each pad + if self.game.settings.ground_start_trucks_roadbase: + self.m.vehicle_group( + country=country, + name=(name + "_fuel"), + _type=tanker_type, + position=pad.position.point_from_heading( + ground_spawn[0].heading.degrees + 90, 35 + ), + group_size=1, + heading=pad.heading + 315, + move_formation=PointAction.OffRoad, + ) + self.m.vehicle_group( + country=country, + name=(name + "_ammo"), + _type=ammo_truck_type, + position=pad.position.point_from_heading( + ground_spawn[0].heading.degrees + 90, 35 + ).point_from_heading(ground_spawn[0].heading.degrees + 180, 10), + group_size=1, + heading=pad.heading + 315, + move_formation=PointAction.OffRoad, + ) + else: + self.m.static_group( + country=country, + name=(name + "_fuel"), + _type=Fortification.FARP_Fuel_Depot, + position=pad.position.point_from_heading( + ground_spawn[0].heading.degrees + 90, 35 + ), + heading=pad.heading + 270, + ) + self.m.static_group( + country=country, + name=(name + "_ammo"), + _type=Fortification.FARP_Ammo_Dump_Coating, + position=pad.position.point_from_heading( + ground_spawn[0].heading.degrees + 90, 35 + ).point_from_heading(ground_spawn[0].heading.degrees + 180, 10), + heading=pad.heading + 180, + ) + if self.game.settings.ground_start_ground_power_trucks_roadbase: + self.m.vehicle_group( + country=country, + name=(name + "_power"), + _type=power_truck_type, + position=pad.position.point_from_heading( + ground_spawn[0].heading.degrees + 90, 35 + ).point_from_heading(ground_spawn[0].heading.degrees + 180, 20), + group_size=1, + heading=pad.heading + 315, + move_formation=PointAction.OffRoad, + ) def generate(self) -> None: try: @@ -1069,6 +1114,14 @@ class GroundSpawnLargeGenerator: country.id ) + warehouse = Airport( + pad.position, + self.m.terrain, + ).dict() + warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" + # configure dynamic spawn + hot start of DS, plus dynamic cargo? + self.m.warehouses.warehouses[pad.id] = warehouse + # Generate a FARP Ammo and Fuel stack for each pad if self.game.settings.ground_start_trucks: self.m.vehicle_group( @@ -1186,61 +1239,88 @@ class GroundSpawnGenerator: country.id ) - # Generate a FARP Ammo and Fuel stack for each pad - if self.game.settings.ground_start_trucks: - self.m.vehicle_group( - country=country, - name=(name + "_fuel"), - _type=tanker_type, - position=pad.position.point_from_heading( - vtol_pad[0].heading.degrees - 175, 35 - ), - group_size=1, - heading=pad.heading + 45, - move_formation=PointAction.OffRoad, - ) - self.m.vehicle_group( - country=country, - name=(name + "_ammo"), - _type=ammo_truck_type, - position=pad.position.point_from_heading( - vtol_pad[0].heading.degrees - 185, 35 - ), - group_size=1, - heading=pad.heading + 45, - move_formation=PointAction.OffRoad, - ) + if self.game.settings.ground_start_airbase_statics_farps_remove and isinstance( + self.cp, Airfield + ): + cull_farp_statics = True + elif self.game.position_culled(vtol_pad[0]): + cull_farp_statics = True + if self.cp.coalition.player: + for package in self.cp.coalition.ato.packages: + for flight in package.flights: + if flight.squadron.location == self.cp: + cull_farp_statics = False + break + elif flight.divert and flight.divert == self.cp: + cull_farp_statics = False + break else: - self.m.static_group( - country=country, - name=(name + "_fuel"), - _type=Fortification.FARP_Fuel_Depot, - position=pad.position.point_from_heading( - vtol_pad[0].heading.degrees - 180, 45 - ), - heading=pad.heading, - ) - self.m.static_group( - country=country, - name=(name + "_ammo"), - _type=Fortification.FARP_Ammo_Dump_Coating, - position=pad.position.point_from_heading( - vtol_pad[0].heading.degrees - 180, 35 - ), - heading=pad.heading + 270, - ) - if self.game.settings.ground_start_ground_power_trucks: - self.m.vehicle_group( - country=country, - name=(name + "_power"), - _type=power_truck_type, - position=pad.position.point_from_heading( - vtol_pad[0].heading.degrees - 185, 35 - ), - group_size=1, - heading=pad.heading + 45, - move_formation=PointAction.OffRoad, - ) + cull_farp_statics = False + + if not cull_farp_statics: + warehouse = Airport( + pad.position, + self.m.terrain, + ).dict() + warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" + # configure dynamic spawn + hot start of DS, plus dynamic cargo? + self.m.warehouses.warehouses[pad.id] = warehouse + + # Generate a FARP Ammo and Fuel stack for each pad + if self.game.settings.ground_start_trucks: + self.m.vehicle_group( + country=country, + name=(name + "_fuel"), + _type=tanker_type, + position=pad.position.point_from_heading( + vtol_pad[0].heading.degrees - 175, 35 + ), + group_size=1, + heading=pad.heading + 45, + move_formation=PointAction.OffRoad, + ) + self.m.vehicle_group( + country=country, + name=(name + "_ammo"), + _type=ammo_truck_type, + position=pad.position.point_from_heading( + vtol_pad[0].heading.degrees - 185, 35 + ), + group_size=1, + heading=pad.heading + 45, + move_formation=PointAction.OffRoad, + ) + else: + self.m.static_group( + country=country, + name=(name + "_fuel"), + _type=Fortification.FARP_Fuel_Depot, + position=pad.position.point_from_heading( + vtol_pad[0].heading.degrees - 180, 45 + ), + heading=pad.heading, + ) + self.m.static_group( + country=country, + name=(name + "_ammo"), + _type=Fortification.FARP_Ammo_Dump_Coating, + position=pad.position.point_from_heading( + vtol_pad[0].heading.degrees - 180, 35 + ), + heading=pad.heading + 270, + ) + if self.game.settings.ground_start_ground_power_trucks: + self.m.vehicle_group( + country=country, + name=(name + "_power"), + _type=power_truck_type, + position=pad.position.point_from_heading( + vtol_pad[0].heading.degrees - 185, 35 + ), + group_size=1, + heading=pad.heading + 45, + move_formation=PointAction.OffRoad, + ) def generate(self) -> None: try: diff --git a/game/persistency.py b/game/persistency.py index f196bf33..d32d2aa6 100644 --- a/game/persistency.py +++ b/game/persistency.py @@ -154,6 +154,10 @@ def save_dir() -> Path: return base_path() / "Retribution" / "Saves" +def pre_pretense_backups_dir() -> Path: + return save_dir() / "PrePretenseBackups" + + def server_port() -> int: global _server_port return _server_port diff --git a/game/pretense/pretenseaircraftgenerator.py b/game/pretense/pretenseaircraftgenerator.py new file mode 100644 index 00000000..e297901f --- /dev/null +++ b/game/pretense/pretenseaircraftgenerator.py @@ -0,0 +1,1073 @@ +from __future__ import annotations + +import logging +import random +from datetime import datetime +from functools import cached_property +from typing import Any, Dict, List, TYPE_CHECKING, Tuple, Optional + +from dcs import Point +from dcs.country import Country +from dcs.mission import Mission +from dcs.terrain import NoParkingSlotError +from dcs.unitgroup import FlyingGroup, StaticGroup + +from game.ato.airtaaskingorder import AirTaskingOrder +from game.ato.flight import Flight +from game.ato.flightstate import WaitingForStart, Navigating +from game.ato.flighttype import FlightType +from game.ato.package import Package +from game.ato.starttype import StartType +from game.coalition import Coalition +from game.data.weapons import WeaponType +from game.dcs.aircrafttype import AircraftType +from game.lasercodes.lasercoderegistry import LaserCodeRegistry +from game.missiongenerator.aircraft.flightdata import FlightData +from game.missiongenerator.missiondata import MissionData +from game.pretense.pretenseflightgroupconfigurator import ( + PretenseFlightGroupConfigurator, +) +from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator +from game.radio.radios import RadioRegistry +from game.radio.tacan import TacanRegistry +from game.runways import RunwayData +from game.settings import Settings +from game.squadrons import AirWing +from game.squadrons import Squadron +from game.theater.controlpoint import ( + ControlPoint, + OffMapSpawn, + ParkingType, + Airfield, + Carrier, + Lha, +) +from game.theater.theatergroundobject import EwrGroundObject, SamGroundObject +from game.unitmap import UnitMap + +if TYPE_CHECKING: + from game import Game + + +PRETENSE_SQUADRON_DEF_RETRIES = 100 +PRETENSE_AI_AWACS_PER_FLIGHT = 1 +PRETENSE_AI_TANKERS_PER_FLIGHT = 1 +PRETENSE_PLAYER_AIRCRAFT_PER_FLIGHT = 1 + + +class PretenseAircraftGenerator: + def __init__( + self, + mission: Mission, + settings: Settings, + game: Game, + time: datetime, + radio_registry: RadioRegistry, + tacan_registry: TacanRegistry, + laser_code_registry: LaserCodeRegistry, + unit_map: UnitMap, + mission_data: MissionData, + helipads: dict[ControlPoint, list[StaticGroup]], + ground_spawns_roadbase: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], + ground_spawns_large: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], + ground_spawns: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], + ) -> None: + self.mission = mission + self.settings = settings + self.game = game + self.time = time + self.radio_registry = radio_registry + self.tacan_registy = tacan_registry + self.laser_code_registry = laser_code_registry + self.unit_map = unit_map + self.flights: List[FlightData] = [] + self.mission_data = mission_data + self.helipads = helipads + self.ground_spawns_roadbase = ground_spawns_roadbase + self.ground_spawns_large = ground_spawns_large + self.ground_spawns = ground_spawns + + self.ewrj_package_dict: Dict[int, List[FlyingGroup[Any]]] = {} + self.ewrj = settings.plugins.get("ewrj") + self.need_ecm = settings.plugin_option("ewrj.ecm_required") + + @cached_property + def use_client(self) -> bool: + """True if Client should be used instead of Player.""" + """Pretense should always use Client slots.""" + return True + + @staticmethod + def client_slots_in_ato(ato: AirTaskingOrder) -> int: + total = 0 + for package in ato.packages: + for flight in package.flights: + total += flight.client_count + return total + + def clear_parking_slots(self) -> None: + for cp in self.game.theater.controlpoints: + for parking_slot in cp.parking_slots: + parking_slot.unit_id = None + + def find_pretense_cargo_plane_cp(self, cp: ControlPoint) -> ControlPoint: + """ + Finds the location (ControlPoint) for Pretense off-map transport planes. + """ + distance_to_flot = 0.0 + offmap_transport_cp_id = cp.id + parking_type = ParkingType( + fixed_wing=True, fixed_wing_stol=True, rotary_wing=False + ) + for front_line_cp in self.game.theater.controlpoints: + if isinstance(front_line_cp, OffMapSpawn): + continue + for front_line in self.game.theater.conflicts(): + if front_line_cp.captured == cp.captured: + if ( + front_line_cp.total_aircraft_parking(parking_type) > 0 + and front_line_cp.position.distance_to_point( + front_line.position + ) + > distance_to_flot + ): + distance_to_flot = front_line_cp.position.distance_to_point( + front_line.position + ) + offmap_transport_cp_id = front_line_cp.id + return self.game.theater.find_control_point_by_id(offmap_transport_cp_id) + + def should_generate_pretense_transports_at( + self, control_point: ControlPoint + ) -> bool: + """ + Returns a boolean, telling whether a transport helicopter squadron + should be generated at control point from the faction squadron definitions. + + This helps to ensure that the faction has at least one transport helicopter at each control point. + """ + autogenerate_transport_helicopter_squadron = True + for squadron in control_point.squadrons: + if squadron.aircraft.helicopter and ( + squadron.aircraft.capable_of(FlightType.TRANSPORT) + or squadron.aircraft.capable_of(FlightType.AIR_ASSAULT) + ): + autogenerate_transport_helicopter_squadron = False + return autogenerate_transport_helicopter_squadron + + def number_of_pretense_cargo_plane_sq_for(self, air_wing: AirWing) -> int: + """ + Returns how many Pretense cargo plane squadrons a specific coalition has. + This is used to define how many such squadrons should be generated from + the faction squadron definitions. + + This helps to ensure that the faction has enough cargo plane squadrons. + """ + number_of_pretense_cargo_plane_squadrons = 0 + for aircraft_type in air_wing.squadrons: + for squadron in air_wing.squadrons[aircraft_type]: + if not squadron.aircraft.helicopter and ( + squadron.aircraft.capable_of(FlightType.TRANSPORT) + or squadron.aircraft.capable_of(FlightType.AIR_ASSAULT) + ): + number_of_pretense_cargo_plane_squadrons += 1 + return number_of_pretense_cargo_plane_squadrons + + def generate_pretense_squadron( + self, + cp: ControlPoint, + coalition: Coalition, + flight_type: FlightType, + fixed_wing: bool, + num_retries: int, + ) -> Optional[Squadron]: + """ + Generates a Pretense squadron from the faction squadron definitions. Use FlightType AIR_ASSAULT + for Pretense supply helicopters and TRANSPORT for off-map cargo plane squadrons. + + Retribution does not differentiate between fixed wing and rotary wing transport squadron definitions, which + is why there is a retry mechanism in case the wrong type is returned. Use fixed_wing False + for Pretense supply helicopters and fixed_wing True for off-map cargo plane squadrons. + + TODO: Find out if Pretense can handle rotary wing "cargo planes". + """ + + squadron_def = coalition.air_wing.squadron_def_generator.generate_for_task( + flight_type, cp + ) + for retries in range(num_retries): + if squadron_def is None or fixed_wing == squadron_def.aircraft.helicopter: + squadron_def = ( + coalition.air_wing.squadron_def_generator.generate_for_task( + flight_type, cp + ) + ) + + # Failed, stop here + if squadron_def is None: + return None + + squadron = Squadron.create_from( + squadron_def, + flight_type, + 2, + cp, + coalition, + self.game, + ) + if squadron.aircraft not in coalition.air_wing.squadrons: + coalition.air_wing.squadrons[squadron.aircraft] = list() + coalition.air_wing.add_squadron(squadron) + return squadron + + def generate_pretense_squadron_for( + self, + aircraft_type: AircraftType, + cp: ControlPoint, + coalition: Coalition, + ) -> Optional[Squadron]: + """ + Generates a Pretense squadron from the faction squadron definitions for the designated + AircraftType. Use FlightType AIR_ASSAULT + for Pretense supply helicopters and TRANSPORT for off-map cargo plane squadrons. + """ + + squadron_def = coalition.air_wing.squadron_def_generator.generate_for_aircraft( + aircraft_type + ) + flight_type = random.choice(list(squadron_def.auto_assignable_mission_types)) + if flight_type == FlightType.ESCORT and aircraft_type.helicopter: + flight_type = FlightType.CAS + if flight_type in ( + FlightType.INTERCEPTION, + FlightType.ESCORT, + FlightType.SWEEP, + ): + flight_type = FlightType.BARCAP + if flight_type in (FlightType.SEAD_ESCORT, FlightType.SEAD_SWEEP): + flight_type = FlightType.SEAD + if flight_type == FlightType.ANTISHIP: + flight_type = FlightType.STRIKE + if flight_type == FlightType.TRANSPORT: + flight_type = FlightType.AIR_ASSAULT + squadron = Squadron.create_from( + squadron_def, + flight_type, + 2, + cp, + coalition, + self.game, + ) + if squadron.aircraft not in coalition.air_wing.squadrons: + coalition.air_wing.squadrons[squadron.aircraft] = list() + coalition.air_wing.add_squadron(squadron) + return squadron + + def generate_pretense_aircraft( + self, cp: ControlPoint, ato: AirTaskingOrder + ) -> None: + """ + Plans and generates AI aircraft groups/packages for Pretense. + + Aircraft generation is done by walking the control points which will be made into + Pretense "zones" and spawning flights for different missions. + After the flight is generated the package is added to the ATO so the flights + can be configured. + + Args: + cp: Control point to generate aircraft for. + ato: The ATO to generate aircraft for. + """ + num_of_sead = 0 + num_of_cas = 0 + num_of_bai = 0 + num_of_strike = 0 + num_of_cap = 0 + sead_tasks = [FlightType.SEAD, FlightType.SEAD_SWEEP, FlightType.SEAD_ESCORT] + strike_tasks = [ + FlightType.STRIKE, + ] + patrol_tasks = [ + FlightType.BARCAP, + FlightType.TARCAP, + FlightType.ESCORT, + FlightType.INTERCEPTION, + ] + sead_capable_cp = False + cas_capable_cp = False + bai_capable_cp = False + strike_capable_cp = False + patrol_capable_cp = False + + # First check what are the capabilities of the squadrons on this CP + for squadron in cp.squadrons: + for task in sead_tasks: + if ( + task in squadron.auto_assignable_mission_types + or FlightType.DEAD in squadron.auto_assignable_mission_types + ): + sead_capable_cp = True + for task in strike_tasks: + if task in squadron.auto_assignable_mission_types: + if not squadron.aircraft.helicopter: + strike_capable_cp = True + for task in patrol_tasks: + if task in squadron.auto_assignable_mission_types: + if not squadron.aircraft.helicopter: + patrol_capable_cp = True + if FlightType.CAS in squadron.auto_assignable_mission_types: + cas_capable_cp = True + if FlightType.BAI in squadron.auto_assignable_mission_types: + bai_capable_cp = True + + random_squadron_list = list(cp.squadrons) + random.shuffle(random_squadron_list) + # Then plan transports, AEWC and tankers + for squadron in random_squadron_list: + # Intentionally don't spawn anything at OffMapSpawns in Pretense + if isinstance(squadron.location, OffMapSpawn): + continue + if cp.coalition != squadron.coalition: + continue + + mission_types = squadron.auto_assignable_mission_types + aircraft_per_flight = 1 + if squadron.aircraft.helicopter and ( + FlightType.TRANSPORT in mission_types + or FlightType.AIR_ASSAULT in mission_types + ): + flight_type = FlightType.AIR_ASSAULT + elif not squadron.aircraft.helicopter and ( + FlightType.TRANSPORT in mission_types + or FlightType.AIR_ASSAULT in mission_types + ): + flight_type = FlightType.TRANSPORT + elif FlightType.AEWC in mission_types: + flight_type = FlightType.AEWC + aircraft_per_flight = PRETENSE_AI_AWACS_PER_FLIGHT + elif FlightType.REFUELING in mission_types: + flight_type = FlightType.REFUELING + aircraft_per_flight = PRETENSE_AI_TANKERS_PER_FLIGHT + else: + continue + + self.generate_pretense_flight( + ato, cp, squadron, aircraft_per_flight, flight_type + ) + random.shuffle(random_squadron_list) + # Then plan SEAD and DEAD, if capable + if sead_capable_cp: + while num_of_sead < self.game.settings.pretense_sead_flights_per_cp: + # Intentionally don't spawn anything at OffMapSpawns in Pretense + if isinstance(cp, OffMapSpawn): + break + for squadron in random_squadron_list: + if cp.coalition != squadron.coalition: + continue + if num_of_sead >= self.game.settings.pretense_sead_flights_per_cp: + break + + mission_types = squadron.auto_assignable_mission_types + if ( + ( + FlightType.SEAD in mission_types + or FlightType.SEAD_SWEEP in mission_types + or FlightType.SEAD_ESCORT in mission_types + ) + and num_of_sead + < self.game.settings.pretense_sead_flights_per_cp + ): + flight_type = FlightType.SEAD + num_of_sead += 1 + aircraft_per_flight = ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + elif ( + FlightType.DEAD in mission_types + and num_of_sead + < self.game.settings.pretense_sead_flights_per_cp + ): + flight_type = FlightType.DEAD + num_of_sead += 1 + aircraft_per_flight = ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + else: + continue + self.generate_pretense_flight( + ato, cp, squadron, aircraft_per_flight, flight_type + ) + random.shuffle(random_squadron_list) + # Then plan Strike, if capable + if strike_capable_cp: + while num_of_strike < self.game.settings.pretense_strike_flights_per_cp: + # Intentionally don't spawn anything at OffMapSpawns in Pretense + if isinstance(cp, OffMapSpawn): + break + for squadron in random_squadron_list: + if cp.coalition != squadron.coalition: + continue + if ( + num_of_strike + >= self.game.settings.pretense_strike_flights_per_cp + ): + break + + mission_types = squadron.auto_assignable_mission_types + for task in strike_tasks: + if task in mission_types and not squadron.aircraft.helicopter: + flight_type = FlightType.STRIKE + num_of_strike += 1 + aircraft_per_flight = ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + self.generate_pretense_flight( + ato, cp, squadron, aircraft_per_flight, flight_type + ) + break + random.shuffle(random_squadron_list) + # Then plan air-to-air, if capable + if patrol_capable_cp: + while num_of_cap < self.game.settings.pretense_barcap_flights_per_cp: + # Intentionally don't spawn anything at OffMapSpawns in Pretense + if isinstance(cp, OffMapSpawn): + break + for squadron in random_squadron_list: + if cp.coalition != squadron.coalition: + continue + if num_of_cap >= self.game.settings.pretense_barcap_flights_per_cp: + break + + mission_types = squadron.auto_assignable_mission_types + for task in patrol_tasks: + if task in mission_types and not squadron.aircraft.helicopter: + flight_type = FlightType.BARCAP + num_of_cap += 1 + aircraft_per_flight = ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + self.generate_pretense_flight( + ato, cp, squadron, aircraft_per_flight, flight_type + ) + break + random.shuffle(random_squadron_list) + # Then plan CAS, if capable + if cas_capable_cp: + while num_of_cas < self.game.settings.pretense_cas_flights_per_cp: + # Intentionally don't spawn anything at OffMapSpawns in Pretense + if isinstance(cp, OffMapSpawn): + break + for squadron in random_squadron_list: + if cp.coalition != squadron.coalition: + continue + if num_of_cas >= self.game.settings.pretense_cas_flights_per_cp: + break + + mission_types = squadron.auto_assignable_mission_types + if ( + squadron.aircraft.helicopter + and (FlightType.ESCORT in mission_types) + ) or (FlightType.CAS in mission_types): + flight_type = FlightType.CAS + num_of_cas += 1 + aircraft_per_flight = ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + self.generate_pretense_flight( + ato, cp, squadron, aircraft_per_flight, flight_type + ) + random.shuffle(random_squadron_list) + # And finally, plan BAI, if capable + if bai_capable_cp: + while num_of_bai < self.game.settings.pretense_bai_flights_per_cp: + # Intentionally don't spawn anything at OffMapSpawns in Pretense + if isinstance(cp, OffMapSpawn): + break + for squadron in random_squadron_list: + if cp.coalition != squadron.coalition: + continue + if num_of_bai >= self.game.settings.pretense_bai_flights_per_cp: + break + + mission_types = squadron.auto_assignable_mission_types + if FlightType.BAI in mission_types: + flight_type = FlightType.BAI + num_of_bai += 1 + aircraft_per_flight = ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + self.generate_pretense_flight( + ato, cp, squadron, aircraft_per_flight, flight_type + ) + + return + + def generate_pretense_flight( + self, + ato: AirTaskingOrder, + cp: ControlPoint, + squadron: Squadron, + aircraft_per_flight: int, + flight_type: FlightType, + ) -> None: + squadron.owned_aircraft += self.game.settings.pretense_ai_aircraft_per_flight + squadron.untasked_aircraft += self.game.settings.pretense_ai_aircraft_per_flight + squadron.populate_for_turn_0(False) + package = Package(cp, squadron.flight_db, auto_asap=False) + if flight_type == FlightType.TRANSPORT: + flight = Flight( + package, + squadron, + aircraft_per_flight, + FlightType.PRETENSE_CARGO, + StartType.IN_FLIGHT, + divert=cp, + ) + package.add_flight(flight) + flight.state = Navigating(flight, self.game.settings, waypoint_index=1) + else: + flight = Flight( + package, + squadron, + aircraft_per_flight, + flight_type, + StartType.COLD, + divert=cp, + ) + if flight.roster is not None and flight.roster.player_count > 0: + flight.start_type = ( + squadron.coalition.game.settings.default_start_type_client + ) + else: + flight.start_type = squadron.coalition.game.settings.default_start_type + package.add_flight(flight) + flight.state = WaitingForStart( + flight, self.game.settings, self.game.conditions.start_time + ) + + print( + f"Generated flight for {flight_type} flying {squadron.aircraft.display_name} at {squadron.location.name}" + ) + ato.add_package(package) + + def generate_pretense_aircraft_for_other_side( + self, cp: ControlPoint, coalition: Coalition, ato: AirTaskingOrder + ) -> None: + """ + Plans and generates AI aircraft groups/packages for Pretense + for the other side, which doesn't initially hold this control point. + + Aircraft generation is done by walking the control points which will be made into + Pretense "zones" and spawning flights for different missions. + After the flight is generated the package is added to the ATO so the flights + can be configured. + + Args: + cp: Control point to generate aircraft for. + coalition: Coalition to generate aircraft for. + ato: The ATO to generate aircraft for. + """ + + aircraft_per_flight = 1 + squadron: Optional[Squadron] = None + if (cp.has_helipads or isinstance(cp, Airfield)) and not cp.is_fleet: + flight_type = FlightType.AIR_ASSAULT + squadron = self.generate_pretense_squadron( + cp, + coalition, + flight_type, + False, + PRETENSE_SQUADRON_DEF_RETRIES, + ) + if squadron is not None: + squadron.owned_aircraft += ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + squadron.untasked_aircraft += ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + squadron.populate_for_turn_0(False) + package = Package(cp, squadron.flight_db, auto_asap=False) + flight = Flight( + package, + squadron, + aircraft_per_flight, + flight_type, + StartType.COLD, + divert=cp, + ) + print( + f"Generated flight for {flight_type} flying {squadron.aircraft.display_name} at {squadron.location.name}" + ) + + package.add_flight(flight) + flight.state = WaitingForStart( + flight, self.game.settings, self.game.conditions.start_time + ) + ato.add_package(package) + if isinstance(cp, Airfield): + # Generate SEAD flight + flight_type = FlightType.SEAD + aircraft_per_flight = self.game.settings.pretense_ai_aircraft_per_flight + squadron = self.generate_pretense_squadron( + cp, + coalition, + flight_type, + True, + PRETENSE_SQUADRON_DEF_RETRIES, + ) + if squadron is None: + squadron = self.generate_pretense_squadron( + cp, + coalition, + FlightType.DEAD, + True, + PRETENSE_SQUADRON_DEF_RETRIES, + ) + if squadron is not None: + squadron.owned_aircraft += ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + squadron.untasked_aircraft += ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + squadron.populate_for_turn_0(False) + package = Package(cp, squadron.flight_db, auto_asap=False) + flight = Flight( + package, + squadron, + aircraft_per_flight, + flight_type, + StartType.COLD, + divert=cp, + ) + print( + f"Generated flight for {flight_type} flying {squadron.aircraft.display_name} at {squadron.location.name}" + ) + + package.add_flight(flight) + flight.state = WaitingForStart( + flight, self.game.settings, self.game.conditions.start_time + ) + ato.add_package(package) + + # Generate CAS flight + flight_type = FlightType.CAS + aircraft_per_flight = self.game.settings.pretense_ai_aircraft_per_flight + squadron = self.generate_pretense_squadron( + cp, + coalition, + flight_type, + True, + PRETENSE_SQUADRON_DEF_RETRIES, + ) + if squadron is None: + squadron = self.generate_pretense_squadron( + cp, + coalition, + FlightType.BAI, + True, + PRETENSE_SQUADRON_DEF_RETRIES, + ) + if squadron is not None: + squadron.owned_aircraft += ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + squadron.untasked_aircraft += ( + self.game.settings.pretense_ai_aircraft_per_flight + ) + squadron.populate_for_turn_0(False) + package = Package(cp, squadron.flight_db, auto_asap=False) + flight = Flight( + package, + squadron, + aircraft_per_flight, + flight_type, + StartType.COLD, + divert=cp, + ) + print( + f"Generated flight for {flight_type} flying {squadron.aircraft.display_name} at {squadron.location.name}" + ) + + package.add_flight(flight) + flight.state = WaitingForStart( + flight, self.game.settings, self.game.conditions.start_time + ) + ato.add_package(package) + if squadron is not None: + if flight.roster is not None and flight.roster.player_count > 0: + flight.start_type = ( + squadron.coalition.game.settings.default_start_type_client + ) + else: + flight.start_type = squadron.coalition.game.settings.default_start_type + return + + def generate_pretense_aircraft_for_players( + self, cp: ControlPoint, coalition: Coalition, ato: AirTaskingOrder + ) -> None: + """ + Plans and generates player piloted aircraft groups/packages for Pretense. + + Aircraft generation is done by walking the control points which will be made into + Pretense "zones" and spawning flights for different missions. + After the flight is generated the package is added to the ATO so the flights + can be configured. + + Args: + cp: Control point to generate aircraft for. + coalition: Coalition to generate aircraft for. + ato: The ATO to generate aircraft for. + """ + + aircraft_per_flight = PRETENSE_PLAYER_AIRCRAFT_PER_FLIGHT + random_aircraft_list = list(coalition.faction.aircraft) + random.shuffle(random_aircraft_list) + for aircraft_type in random_aircraft_list: + # Don't generate any player flights for non-flyable types (obviously) + if not aircraft_type.flyable: + continue + if not cp.can_operate(aircraft_type): + continue + + for i in range(self.game.settings.pretense_player_flights_per_type): + squadron = self.generate_pretense_squadron_for( + aircraft_type, + cp, + coalition, + ) + if squadron is not None: + squadron.owned_aircraft += PRETENSE_PLAYER_AIRCRAFT_PER_FLIGHT + squadron.untasked_aircraft += PRETENSE_PLAYER_AIRCRAFT_PER_FLIGHT + squadron.populate_for_turn_0(False) + for pilot in squadron.pilot_pool: + pilot.player = True + package = Package(cp, squadron.flight_db, auto_asap=False) + primary_task = squadron.primary_task + if primary_task in [FlightType.OCA_AIRCRAFT, FlightType.OCA_RUNWAY]: + primary_task = FlightType.STRIKE + flight = Flight( + package, + squadron, + aircraft_per_flight, + primary_task, + squadron.coalition.game.settings.default_start_type_client, + divert=cp, + ) + for roster_pilot in flight.roster.members: + if roster_pilot.pilot is not None: + roster_pilot.pilot.player = True + print( + f"Generated flight for {squadron.primary_task} flying {squadron.aircraft.display_name} at {squadron.location.name}. Pilot client count: {flight.client_count}" + ) + + package.add_flight(flight) + flight.state = WaitingForStart( + flight, self.game.settings, self.game.conditions.start_time + ) + ato.add_package(package) + + return + + def initialize_pretense_data_structures(self, cp: ControlPoint) -> None: + """ + Ensures that the data structures used to pass flight group information + to the Pretense init script lua are initialized for use in + PretenseFlightGroupSpawner and PretenseLuaGenerator. + + Args: + cp: Control point to generate aircraft for. + flight: The current flight being generated. + """ + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp.name) + + for side in range(1, 3): + if cp_name_trimmed not in cp.coalition.game.pretense_air[side]: + cp.coalition.game.pretense_air[side][cp_name_trimmed] = {} + return + + def initialize_pretense_data_structures_for_flight( + self, cp: ControlPoint, flight: Flight + ) -> None: + """ + Ensures that the data structures used to pass flight group information + to the Pretense init script lua are initialized for use in + PretenseFlightGroupSpawner and PretenseLuaGenerator. + + Args: + cp: Control point to generate aircraft for. + flight: The current flight being generated. + """ + flight_type = flight.flight_type + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp.name) + + for side in range(1, 3): + if cp_name_trimmed not in flight.coalition.game.pretense_air[side]: + flight.coalition.game.pretense_air[side][cp_name_trimmed] = {} + if ( + flight_type + not in flight.coalition.game.pretense_air[side][cp_name_trimmed] + ): + flight.coalition.game.pretense_air[side][cp_name_trimmed][ + flight_type + ] = list() + return + + def generate_flights( + self, + country: Country, + cp: ControlPoint, + ato: AirTaskingOrder, + ) -> None: + """Adds aircraft to the mission for every flight in the ATO. + + Args: + country: The country from the mission to use for this ATO. + cp: Control point to generate aircraft for. + ato: The ATO to generate aircraft for. + dynamic_runways: Runway data for carriers and FARPs. + """ + self.initialize_pretense_data_structures(cp) + + is_player = True + if country == cp.coalition.faction.country: + offmap_transport_cp = self.find_pretense_cargo_plane_cp(cp) + + if ( + cp.has_helipads + or isinstance(cp, Airfield) + or isinstance(cp, Carrier) + or isinstance(cp, Lha) + ) and self.should_generate_pretense_transports_at(cp): + self.generate_pretense_squadron( + cp, + cp.coalition, + FlightType.AIR_ASSAULT, + False, + PRETENSE_SQUADRON_DEF_RETRIES, + ) + num_of_cargo_sq_to_generate = ( + self.game.settings.pretense_ai_cargo_planes_per_side + - self.number_of_pretense_cargo_plane_sq_for(cp.coalition.air_wing) + ) + for i in range(num_of_cargo_sq_to_generate): + self.generate_pretense_squadron( + offmap_transport_cp, + offmap_transport_cp.coalition, + FlightType.TRANSPORT, + True, + PRETENSE_SQUADRON_DEF_RETRIES, + ) + + self.generate_pretense_aircraft(cp, ato) + else: + coalition = ( + self.game.coalition_for(is_player) + if country == self.game.coalition_for(is_player).faction.country + else self.game.coalition_for(False) + ) + self.generate_pretense_aircraft_for_other_side(cp, coalition, ato) + + if country == self.game.coalition_for(is_player).faction.country: + if not isinstance(cp, OffMapSpawn): + coalition = self.game.coalition_for(is_player) + self.generate_pretense_aircraft_for_players(cp, coalition, ato) + + self._reserve_frequencies_and_tacan(ato) + + def generate_packages( + self, + country: Country, + ato: AirTaskingOrder, + dynamic_runways: Dict[str, RunwayData], + ) -> None: + for package in ato.packages: + logging.info( + f"Generating package for target: {package.target.name}, has_players: {package.has_players}" + ) + if not package.flights: + continue + for flight in package.flights: + self.initialize_pretense_data_structures_for_flight( + flight.departure, flight + ) + logging.info( + f"Generating flight in {flight.coalition.faction.name} package" + f" {flight.squadron.aircraft} {flight.flight_type} for target: {package.target.name}," + f" departure: {flight.departure.name}" + ) + + if flight.alive: + if not flight.squadron.location.runway_is_operational(): + logging.warning( + f"Runway not operational, skipping flight: {flight.flight_type}" + ) + flight.return_pilots_and_aircraft() + continue + logging.info( + f"Generating flight: {flight.unit_type} for {flight.flight_type.name}" + ) + try: + group = self.create_and_configure_flight( + flight, country, dynamic_runways + ) + except NoParkingSlotError: + logging.warning( + f"No room on runway or parking slots for {flight.squadron.aircraft} {flight.flight_type} for target: {package.target.name}. Not generating flight." + ) + return + self.unit_map.add_aircraft(group, flight) + + def create_and_configure_flight( + self, flight: Flight, country: Country, dynamic_runways: Dict[str, RunwayData] + ) -> FlyingGroup[Any]: + from game.pretense.pretenseflightgroupspawner import PretenseFlightGroupSpawner + + """Creates and configures the flight group in the mission.""" + if not country.unused_onboard_numbers: + country.reset_onboard_numbers() + group = PretenseFlightGroupSpawner( + flight, + country, + self.mission, + self.helipads, + self.ground_spawns_roadbase, + self.ground_spawns_large, + self.ground_spawns, + self.mission_data, + ).create_flight_group() + + control_points_to_scan = ( + list(self.game.theater.closest_opposing_control_points()) + + self.game.theater.controlpoints + ) + + if ( + flight.flight_type == FlightType.CAS + or flight.flight_type == FlightType.TARCAP + ): + for conflict in self.game.theater.conflicts(): + flight.package.target = conflict + break + elif flight.flight_type == FlightType.BARCAP: + for cp in control_points_to_scan: + if cp.coalition != flight.coalition or cp == flight.departure: + continue + if flight.package.target != flight.departure: + break + for mission_target in cp.ground_objects: + flight.package.target = mission_target + break + elif ( + flight.flight_type == FlightType.STRIKE + or flight.flight_type == FlightType.BAI + or flight.flight_type == FlightType.ARMED_RECON + ): + for cp in control_points_to_scan: + if cp.coalition == flight.coalition or cp == flight.departure: + continue + if flight.package.target != flight.departure: + break + for mission_target in cp.ground_objects: + if mission_target.alive_unit_count > 0: + flight.package.target = mission_target + break + elif ( + flight.flight_type == FlightType.OCA_RUNWAY + or flight.flight_type == FlightType.OCA_AIRCRAFT + ): + for cp in control_points_to_scan: + if ( + cp.coalition == flight.coalition + or not isinstance(cp, Airfield) + or cp == flight.departure + ): + continue + flight.package.target = cp + break + elif ( + flight.flight_type == FlightType.DEAD + or flight.flight_type == FlightType.SEAD + ): + for cp in control_points_to_scan: + if cp.coalition == flight.coalition or cp == flight.departure: + continue + if flight.package.target != flight.departure: + break + for ground_object in cp.ground_objects: + is_ewr = isinstance(ground_object, EwrGroundObject) + is_sam = isinstance(ground_object, SamGroundObject) + + if is_ewr or is_sam: + flight.package.target = ground_object + break + elif flight.flight_type == FlightType.AIR_ASSAULT: + for cp in control_points_to_scan: + if cp.coalition == flight.coalition or cp == flight.departure: + continue + if flight.is_hercules: + if cp.coalition == flight.coalition or not isinstance(cp, Airfield): + continue + flight.package.target = cp + break + + now = self.game.conditions.start_time + try: + flight.package.set_tot_asap(now) + except: + raise RuntimeError( + f"Pretense flight group {group.name} {flight.squadron.aircraft} {flight.flight_type} for target {flight.package.target} configuration failed. Please check if your Retribution campaign is compatible with Pretense." + ) + + logging.info( + f"Configuring flight {group.name} {flight.squadron.aircraft} {flight.flight_type}, number of players: {flight.client_count}" + ) + self.mission_data.flights.append( + PretenseFlightGroupConfigurator( + flight, + group, + self.game, + self.mission, + self.time, + self.radio_registry, + self.tacan_registy, + self.mission_data, + dynamic_runways, + self.use_client, + ).configure() + ) + + if self.ewrj: + self._track_ewrj_flight(flight, group) + + return group + + def _track_ewrj_flight(self, flight: Flight, group: FlyingGroup[Any]) -> None: + if not self.ewrj_package_dict.get(id(flight.package)): + self.ewrj_package_dict[id(flight.package)] = [] + if ( + flight.package.primary_flight + and flight is flight.package.primary_flight + or flight.client_count + and ( + not self.need_ecm + or flight.any_member_has_weapon_of_type(WeaponType.JAMMER) + ) + ): + self.ewrj_package_dict[id(flight.package)].append(group) + + def _reserve_frequencies_and_tacan(self, ato: AirTaskingOrder) -> None: + for package in ato.packages: + if package.frequency is None: + continue + if package.frequency not in self.radio_registry.allocated_channels: + self.radio_registry.reserve(package.frequency) + for f in package.flights: + if ( + f.frequency + and f.frequency not in self.radio_registry.allocated_channels + ): + self.radio_registry.reserve(f.frequency) + if f.tacan and f.tacan not in self.tacan_registy.allocated_channels: + self.tacan_registy.mark_unavailable(f.tacan) diff --git a/game/pretense/pretenseflightgroupconfigurator.py b/game/pretense/pretenseflightgroupconfigurator.py new file mode 100644 index 00000000..10bc45a4 --- /dev/null +++ b/game/pretense/pretenseflightgroupconfigurator.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any, Optional, TYPE_CHECKING + +from dcs import Mission, Point +from dcs.flyingunit import FlyingUnit +from dcs.unitgroup import FlyingGroup + +from game.ato import Flight, FlightType +from game.ato.flightmember import FlightMember +from game.data.weapons import Pylon +from game.lasercodes.lasercoderegistry import LaserCodeRegistry +from game.missiongenerator.aircraft.aircraftbehavior import AircraftBehavior +from game.missiongenerator.aircraft.aircraftpainter import AircraftPainter +from game.missiongenerator.aircraft.bingoestimator import BingoEstimator +from game.missiongenerator.aircraft.flightdata import FlightData +from game.missiongenerator.aircraft.flightgroupconfigurator import ( + FlightGroupConfigurator, +) +from game.missiongenerator.aircraft.waypoints import WaypointGenerator +from game.missiongenerator.missiondata import MissionData +from game.radio.radios import RadioRegistry +from game.radio.tacan import ( + TacanRegistry, +) +from game.runways import RunwayData + +if TYPE_CHECKING: + from game import Game + + +class PretenseFlightGroupConfigurator(FlightGroupConfigurator): + def __init__( + self, + flight: Flight, + group: FlyingGroup[Any], + game: Game, + mission: Mission, + time: datetime, + radio_registry: RadioRegistry, + tacan_registry: TacanRegistry, + mission_data: MissionData, + dynamic_runways: dict[str, RunwayData], + use_client: bool, + ) -> None: + super().__init__( + flight, + group, + game, + mission, + time, + radio_registry, + tacan_registry, + mission_data, + dynamic_runways, + use_client, + ) + + self.flight = flight + self.group = group + self.game = game + self.mission = mission + self.time = time + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry + self.mission_data = mission_data + self.dynamic_runways = dynamic_runways + self.use_client = use_client + + def configure(self) -> FlightData: + AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group) + AircraftPainter(self.flight, self.group).apply_livery() + self.setup_props() + self.setup_payloads() + self.setup_fuel() + flight_channel = self.setup_radios() + + laser_codes: list[Optional[int]] = [] + for unit, pilot in zip(self.group.units, self.flight.roster.members): + self.configure_flight_member(unit, pilot, laser_codes) + + divert = None + if self.flight.divert is not None: + divert = self.flight.divert.active_runway( + self.game.theater, self.game.conditions, self.dynamic_runways + ) + + mission_start_time, waypoints = WaypointGenerator( + self.flight, + self.group, + self.mission, + self.time, + self.game.settings, + self.mission_data, + ).create_waypoints() + + divert_position: Point | None = None + if self.flight.divert is not None: + divert_position = self.flight.divert.position + bingo_estimator = BingoEstimator( + self.flight.unit_type.fuel_consumption, + self.flight.arrival.position, + divert_position, + self.flight.flight_plan.waypoints, + ) + + self.group.uncontrolled = False + + return FlightData( + package=self.flight.package, + aircraft_type=self.flight.unit_type, + squadron=self.flight.squadron, + flight_type=self.flight.flight_type, + units=self.group.units, + size=len(self.group.units), + friendly=self.flight.departure.captured, + departure_delay=mission_start_time, + departure=self.flight.departure.active_runway( + self.game.theater, self.game.conditions, self.dynamic_runways + ), + arrival=self.flight.arrival.active_runway( + self.game.theater, self.game.conditions, self.dynamic_runways + ), + divert=divert, + waypoints=waypoints, + intra_flight_channel=flight_channel, + bingo_fuel=bingo_estimator.estimate_bingo(), + joker_fuel=bingo_estimator.estimate_joker(), + custom_name=self.flight.custom_name, + laser_codes=laser_codes, + ) + + def setup_payloads(self) -> None: + for unit, member in zip(self.group.units, self.flight.iter_members()): + self.setup_payload(unit, member) + + def setup_payload(self, unit: FlyingUnit, member: FlightMember) -> None: + unit.pylons.clear() + + loadout = member.loadout + + if self.flight.flight_type == FlightType.SEAD: + loadout = member.loadout.default_for_task_and_aircraft( + FlightType.SEAD_SWEEP, self.flight.unit_type.dcs_unit_type + ) + + if self.game.settings.restrict_weapons_by_date: + loadout = loadout.degrade_for_date(self.flight.unit_type, self.game.date) + + for pylon_number, weapon in loadout.pylons.items(): + if weapon is None: + continue + pylon = Pylon.for_aircraft(self.flight.unit_type, pylon_number) + pylon.equip(unit, weapon) diff --git a/game/pretense/pretenseflightgroupspawner.py b/game/pretense/pretenseflightgroupspawner.py new file mode 100644 index 00000000..ace1b3f2 --- /dev/null +++ b/game/pretense/pretenseflightgroupspawner.py @@ -0,0 +1,261 @@ +import logging +import random +from typing import Any, Tuple + +from dcs import Mission +from dcs.country import Country +from dcs.mapping import Vector2, Point +from dcs.terrain import NoParkingSlotError, TheChannel, Falklands +from dcs.terrain.falklands.airports import San_Carlos_FOB, Goose_Green, Gull_Point +from dcs.terrain.thechannel.airports import Manston +from dcs.unitgroup import ( + FlyingGroup, + ShipGroup, + StaticGroup, +) + +from game.ato import Flight +from game.ato.flightstate import InFlight +from game.ato.starttype import StartType +from game.missiongenerator.aircraft.flightgroupspawner import ( + FlightGroupSpawner, + MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL, + MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL, + STACK_SEPARATION, +) +from game.missiongenerator.missiondata import MissionData +from game.naming import NameGenerator +from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint + + +class PretenseNameGenerator(NameGenerator): + @classmethod + def next_pretense_aircraft_name(cls, cp: ControlPoint, flight: Flight) -> str: + cls.aircraft_number += 1 + cp_name_trimmed = cls.pretense_trimmed_cp_name(cp.name) + return "{}-{}-{}".format( + cp_name_trimmed, str(flight.flight_type).lower(), cls.aircraft_number + ) + + @classmethod + def pretense_trimmed_cp_name(cls, cp_name: str) -> str: + cp_name_alnum = "".join([i for i in cp_name.lower() if i.isalnum()]) + cp_name_trimmed = cp_name_alnum.lstrip("1 2 3 4 5 6 7 8 9 0") + cp_name_trimmed = cp_name_trimmed.replace("ä", "a") + cp_name_trimmed = cp_name_trimmed.replace("ö", "o") + cp_name_trimmed = cp_name_trimmed.replace("ø", "o") + return cp_name_trimmed + + +namegen = PretenseNameGenerator +# Air-start AI aircraft which are faster than this on WWII terrains +# 1000 km/h is just above the max speed of the Harrier and Su-25, +# so they will still start normally from grass and dirt strips +WW2_TERRAIN_SUPERSONIC_AI_AIRSTART_SPEED = 1000 + + +class PretenseFlightGroupSpawner(FlightGroupSpawner): + def __init__( + self, + flight: Flight, + country: Country, + mission: Mission, + helipads: dict[ControlPoint, list[StaticGroup]], + ground_spawns_roadbase: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], + ground_spawns_large: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], + ground_spawns: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], + mission_data: MissionData, + ) -> None: + super().__init__( + flight, + country, + mission, + helipads, + ground_spawns_roadbase, + ground_spawns_large, + ground_spawns, + mission_data, + ) + + self.flight = flight + self.country = country + self.mission = mission + self.helipads = helipads + self.ground_spawns_roadbase = ground_spawns_roadbase + self.ground_spawns = ground_spawns + self.mission_data = mission_data + + def insert_into_pretense(self, name: str) -> None: + cp = self.flight.departure + is_player = True + cp_side = ( + 2 + if self.flight.coalition + == self.flight.coalition.game.coalition_for(is_player) + else 1 + ) + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp.name) + + if self.flight.client_count == 0: + self.flight.coalition.game.pretense_air[cp_side][cp_name_trimmed][ + self.flight.flight_type + ].append(name) + try: + self.flight.coalition.game.pretense_air_groups[name] = self.flight + except AttributeError: + self.flight.coalition.game.pretense_air_groups = {} + self.flight.coalition.game.pretense_air_groups[name] = self.flight + + def generate_flight_at_departure(self) -> FlyingGroup[Any]: + cp = self.flight.departure + name = namegen.next_pretense_aircraft_name(cp, self.flight) + + try: + if self.start_type is StartType.IN_FLIGHT: + self.insert_into_pretense(name) + group = self._generate_over_departure(name, cp) + return group + elif isinstance(cp, NavalControlPoint): + group_name = cp.get_carrier_group_name() + carrier_group = self.mission.find_group(group_name) + if not isinstance(carrier_group, ShipGroup): + raise RuntimeError( + f"Carrier group {carrier_group} is a " + f"{carrier_group.__class__.__name__}, expected a ShipGroup" + ) + self.insert_into_pretense(name) + return self._generate_at_group(name, carrier_group) + elif isinstance(cp, Fob): + is_heli = self.flight.squadron.aircraft.helicopter + is_vtol = not is_heli and self.flight.squadron.aircraft.lha_capable + if not is_heli and not is_vtol and not cp.has_ground_spawns: + raise RuntimeError( + f"Cannot spawn fixed-wing aircraft at {cp} because of insufficient ground spawn slots." + ) + pilot_count = len(self.flight.roster.members) + if ( + not is_heli + and self.flight.roster.player_count != pilot_count + and not self.flight.coalition.game.settings.ground_start_ai_planes + ): + raise RuntimeError( + f"Fixed-wing aircraft at {cp} must be piloted by humans exclusively because" + f' the "AI fixed-wing aircraft can use roadbases / bases with only ground' + f' spawns" setting is currently disabled.' + ) + if cp.has_helipads and (is_heli or is_vtol): + self.insert_into_pretense(name) + pad_group = self._generate_at_cp_helipad(name, cp) + if pad_group is not None: + return pad_group + if cp.has_ground_spawns and (self.flight.client_count > 0 or is_heli): + self.insert_into_pretense(name) + pad_group = self._generate_at_cp_ground_spawn(name, cp) + if pad_group is not None: + return pad_group + raise NoParkingSlotError + elif isinstance(cp, Airfield): + is_heli = self.flight.squadron.aircraft.helicopter + is_vtol = not is_heli and self.flight.squadron.aircraft.lha_capable + if cp.has_helipads and is_heli: + self.insert_into_pretense(name) + pad_group = self._generate_at_cp_helipad(name, cp) + if pad_group is not None: + return pad_group + # Air-start supersonic AI aircraft if the campaign is being flown in a WWII terrain + # This will improve these terrains' use in cold war campaigns + if isinstance(cp.theater.terrain, TheChannel) and not isinstance( + cp.dcs_airport, Manston + ): + if ( + self.flight.client_count == 0 + and self.flight.squadron.aircraft.max_speed.speed_in_kph + > WW2_TERRAIN_SUPERSONIC_AI_AIRSTART_SPEED + ): + self.insert_into_pretense(name) + return self._generate_over_departure(name, cp) + # Air-start AI fixed wing (non-VTOL) aircraft if the campaign is being flown in the South Atlantic terrain and + # the airfield is one of the Harrier-only ones in East Falklands. + # This will help avoid AI aircraft from smashing into the end of the runway and exploding. + if isinstance(cp.theater.terrain, Falklands) and ( + isinstance(cp.dcs_airport, San_Carlos_FOB) + or isinstance(cp.dcs_airport, Goose_Green) + or isinstance(cp.dcs_airport, Gull_Point) + ): + if self.flight.client_count == 0 and is_vtol: + self.insert_into_pretense(name) + return self._generate_over_departure(name, cp) + if ( + cp.has_ground_spawns + and len(self.ground_spawns[cp]) + + len(self.ground_spawns_roadbase[cp]) + >= self.flight.count + and (self.flight.client_count > 0 or is_heli) + ): + self.insert_into_pretense(name) + pad_group = self._generate_at_cp_ground_spawn(name, cp) + if pad_group is not None: + return pad_group + self.insert_into_pretense(name) + return self._generate_at_airfield(name, cp) + else: + raise NotImplementedError( + f"Aircraft spawn behavior not implemented for {cp} ({cp.__class__})" + ) + except NoParkingSlotError: + # Generated when there is no place on Runway or on Parking Slots + if self.flight.client_count > 0: + # Don't generate player airstarts + logging.warning( + "No room on runway or parking slots. Not generating a player air-start." + ) + raise NoParkingSlotError + else: + logging.warning( + "No room on runway or parking slots. Starting from the air." + ) + self.flight.start_type = StartType.IN_FLIGHT + self.insert_into_pretense(name) + group = self._generate_over_departure(name, cp) + return group + + def generate_mid_mission(self) -> FlyingGroup[Any]: + assert isinstance(self.flight.state, InFlight) + name = namegen.next_pretense_aircraft_name(self.flight.departure, self.flight) + + speed = self.flight.state.estimate_speed() + pos = self.flight.state.estimate_position() + pos += Vector2(random.randint(100, 1000), random.randint(100, 1000)) + alt, alt_type = self.flight.state.estimate_altitude() + cp = self.flight.squadron.location.id + + if cp not in self.mission_data.cp_stack: + self.mission_data.cp_stack[cp] = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL + + # We don't know where the ground is, so just make sure that any aircraft + # spawning at an MSL altitude is spawned at some minimum altitude. + # https://github.com/dcs-liberation/dcs_liberation/issues/1941 + if alt_type == "BARO" and alt < MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL: + alt = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL + + # Set a minimum AGL value for 'alt' if needed, + # otherwise planes might crash in trees and stuff. + if alt_type == "RADIO" and alt < self.mission_data.cp_stack[cp]: + alt = self.mission_data.cp_stack[cp] + self.mission_data.cp_stack[cp] += STACK_SEPARATION + + self.insert_into_pretense(name) + group = self.mission.flight_group( + country=self.country, + name=name, + aircraft_type=self.flight.unit_type.dcs_unit_type, + airport=None, + position=pos, + altitude=alt.meters, + speed=speed.kph, + maintask=None, + group_size=self.flight.count, + ) + + group.points[0].alt_type = alt_type + return group diff --git a/game/pretense/pretenseluagenerator.py b/game/pretense/pretenseluagenerator.py new file mode 100644 index 00000000..304ce93a --- /dev/null +++ b/game/pretense/pretenseluagenerator.py @@ -0,0 +1,2059 @@ +from __future__ import annotations + +import logging +import os +import random +from abc import ABC, abstractmethod +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING, Optional, List, Type + +from dcs import Mission +from dcs.action import DoScript, DoScriptFile +from dcs.ships import Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal +from dcs.translation import String +from dcs.triggers import TriggerStart +from dcs.unittype import VehicleType, ShipType +from dcs.vehicles import AirDefence, Unarmed + +from game.ato import FlightType +from game.coalition import Coalition +from game.data.units import UnitClass +from game.dcs.aircrafttype import AircraftType +from game.missiongenerator.luagenerator import LuaGenerator +from game.missiongenerator.missiondata import MissionData +from game.plugins import LuaPluginManager +from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator +from game.pretense.pretensetgogenerator import PretenseGroundObjectGenerator +from game.theater import Airfield, OffMapSpawn, TheaterGroundObject +from game.theater.iadsnetwork.iadsrole import IadsRole +from game.utils import escape_string_for_lua +from pydcs_extensions import IRON_DOME_LN, DAVID_SLING_LN + +if TYPE_CHECKING: + from game import Game + +PRETENSE_RED_SIDE = 1 +PRETENSE_BLUE_SIDE = 2 +PRETENSE_NUMBER_OF_ZONES_TO_CONNECT_CARRIERS_TO = 2 + + +@dataclass +class PretenseSam: + name: str + enabled: bool + + def __init__( + self, + name: str, + ) -> None: + self.name = name + self.enabled = False + + +class PretenseLuaGenerator(LuaGenerator): + def __init__( + self, + game: Game, + mission: Mission, + mission_data: MissionData, + ) -> None: + super().__init__( + game, + mission, + mission_data, + ) + + self.game = game + self.mission = mission + self.mission_data = mission_data + self.plugin_scripts: list[str] = [] + + def generate(self) -> None: + ewrj_triggers = [ + x for x in self.mission.triggerrules.triggers if isinstance(x, TriggerStart) + ] + self.generate_pretense_plugin_data() + self.generate_plugin_data() + self.inject_plugins() + for t in ewrj_triggers: + self.mission.triggerrules.triggers.remove(t) + self.mission.triggerrules.triggers.append(t) + + def generate_plugin_data(self) -> None: + lua_data = LuaData("dcsRetribution") + + install_path = lua_data.add_item("installPath") + install_path.set_value(os.path.abspath(".")) + + lua_data.add_item("Airbases") + carriers_object = lua_data.add_item("Carriers") + + for carrier in self.mission_data.carriers: + carrier_item = carriers_object.add_item() + carrier_item.add_key_value("dcsGroupName", carrier.group_name) + carrier_item.add_key_value("unit_name", carrier.unit_name) + carrier_item.add_key_value("callsign", carrier.callsign) + carrier_item.add_key_value("radio", str(carrier.freq.mhz)) + carrier_item.add_key_value( + "tacan", str(carrier.tacan.number) + carrier.tacan.band.name + ) + + tankers_object = lua_data.add_item("Tankers") + for tanker in self.mission_data.tankers: + tanker_item = tankers_object.add_item() + tanker_item.add_key_value("dcsGroupName", tanker.group_name) + tanker_item.add_key_value("callsign", tanker.callsign) + tanker_item.add_key_value("variant", tanker.variant) + tanker_item.add_key_value("radio", str(tanker.freq.mhz)) + if tanker.tacan is not None: + tanker_item.add_key_value( + "tacan", str(tanker.tacan.number) + tanker.tacan.band.name + ) + + awacs_object = lua_data.add_item("AWACs") + for awacs in self.mission_data.awacs: + awacs_item = awacs_object.add_item() + awacs_item.add_key_value("dcsGroupName", awacs.group_name) + awacs_item.add_key_value("callsign", awacs.callsign) + awacs_item.add_key_value("radio", str(awacs.freq.mhz)) + + jtacs_object = lua_data.add_item("JTACs") + for jtac in self.mission_data.jtacs: + jtac_item = jtacs_object.add_item() + jtac_item.add_key_value("dcsGroupName", jtac.group_name) + jtac_item.add_key_value("callsign", jtac.callsign) + jtac_item.add_key_value("zone", jtac.region) + jtac_item.add_key_value("dcsUnit", jtac.unit_name) + jtac_item.add_key_value("laserCode", jtac.code) + jtac_item.add_key_value("radio", str(jtac.freq.mhz)) + jtac_item.add_key_value("modulation", jtac.freq.modulation.name) + + logistics_object = lua_data.add_item("Logistics") + logistics_flights = logistics_object.add_item("flights") + crates_object = logistics_object.add_item("crates") + spawnable_crates: dict[str, str] = {} + transports: list[AircraftType] = [] + for logistic_info in self.mission_data.logistics: + if logistic_info.transport not in transports: + transports.append(logistic_info.transport) + coalition_color = "blue" if logistic_info.blue else "red" + logistics_item = logistics_flights.add_item() + logistics_item.add_data_array("pilot_names", logistic_info.pilot_names) + logistics_item.add_key_value("pickup_zone", logistic_info.pickup_zone) + logistics_item.add_key_value("drop_off_zone", logistic_info.drop_off_zone) + logistics_item.add_key_value("target_zone", logistic_info.target_zone) + logistics_item.add_key_value("side", str(2 if logistic_info.blue else 1)) + logistics_item.add_key_value("logistic_unit", logistic_info.logistic_unit) + logistics_item.add_key_value( + "aircraft_type", logistic_info.transport.dcs_id + ) + logistics_item.add_key_value( + "preload", "true" if logistic_info.preload else "false" + ) + for cargo in logistic_info.cargo: + if cargo.unit_type not in spawnable_crates: + spawnable_crates[cargo.unit_type] = str(200 + len(spawnable_crates)) + crate_weight = spawnable_crates[cargo.unit_type] + for i in range(cargo.amount): + cargo_item = crates_object.add_item() + cargo_item.add_key_value("weight", crate_weight) + cargo_item.add_key_value("coalition", coalition_color) + cargo_item.add_key_value("zone", cargo.spawn_zone) + transport_object = logistics_object.add_item("transports") + for transport in transports: + transport_item = transport_object.add_item() + transport_item.add_key_value("aircraft_type", transport.dcs_id) + transport_item.add_key_value("cabin_size", str(transport.cabin_size)) + transport_item.add_key_value( + "troops", "true" if transport.cabin_size > 0 else "false" + ) + transport_item.add_key_value( + "crates", "true" if transport.can_carry_crates else "false" + ) + spawnable_crates_object = logistics_object.add_item("spawnable_crates") + for unit, weight in spawnable_crates.items(): + crate_item = spawnable_crates_object.add_item() + crate_item.add_key_value("unit", unit) + crate_item.add_key_value("weight", weight) + + target_points = lua_data.add_item("TargetPoints") + for flight in self.mission_data.flights: + if flight.friendly and flight.flight_type in [ + FlightType.ANTISHIP, + FlightType.DEAD, + FlightType.SEAD, + FlightType.STRIKE, + ]: + flight_type = str(flight.flight_type) + flight_target = flight.package.target + if flight_target: + flight_target_name = None + flight_target_type = None + if isinstance(flight_target, TheaterGroundObject): + flight_target_name = flight_target.obj_name + flight_target_type = ( + flight_type + f" TGT ({flight_target.category})" + ) + elif hasattr(flight_target, "name"): + flight_target_name = flight_target.name + flight_target_type = flight_type + " TGT (Airbase)" + target_item = target_points.add_item() + if flight_target_name: + target_item.add_key_value("name", flight_target_name) + if flight_target_type: + target_item.add_key_value("type", flight_target_type) + target_item.add_key_value( + "positionX", str(flight_target.position.x) + ) + target_item.add_key_value( + "positionY", str(flight_target.position.y) + ) + + for cp in self.game.theater.controlpoints: + coalition_object = ( + lua_data.get_or_create_item("BlueAA") + if cp.captured + else lua_data.get_or_create_item("RedAA") + ) + for ground_object in cp.ground_objects: + for g in ground_object.groups: + threat_range = g.max_threat_range() + + if not threat_range: + continue + + aa_item = coalition_object.add_item() + aa_item.add_key_value("name", ground_object.name) + aa_item.add_key_value("range", str(threat_range.meters)) + aa_item.add_key_value("positionX", str(ground_object.position.x)) + aa_item.add_key_value("positionY", str(ground_object.position.y)) + + # Generate IADS Lua Item + iads_object = lua_data.add_item("IADS") + for node in self.game.theater.iads_network.skynet_nodes(self.game): + coalition = iads_object.get_or_create_item("BLUE" if node.player else "RED") + iads_type = coalition.get_or_create_item(node.iads_role.value) + iads_element = iads_type.add_item() + iads_element.add_key_value("dcsGroupName", node.dcs_name) + if node.iads_role in [IadsRole.SAM, IadsRole.SAM_AS_EWR]: + # add additional SkynetProperties to SAM Sites + for property, value in node.properties.items(): + iads_element.add_key_value(property, value) + for role, connections in node.connections.items(): + iads_element.add_data_array(role, connections) + + trigger = TriggerStart(comment="Set DCS Retribution data") + trigger.add_action(DoScript(String(lua_data.create_operations_lua()))) + self.mission.triggerrules.triggers.append(trigger) + + @staticmethod + def generate_sam_from_preset( + preset: str, cp_side_str: str, cp_name_trimmed: str + ) -> str: + lua_string_zones = ( + " presets.defenses." + + cp_side_str + + "." + + preset + + ":extend({ name='" + + cp_name_trimmed + + f"-{preset}-" + + cp_side_str + + "' }),\n" + ) + return lua_string_zones + + def generate_pretense_land_upgrade_supply(self, cp_name: str, cp_side: int) -> str: + lua_string_zones = "" + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp_name) + cp_side_str = "blue" if cp_side == PRETENSE_BLUE_SIDE else "red" + cp = self.game.theater.controlpoints[0] + for loop_cp in self.game.theater.controlpoints: + if loop_cp.name == cp_name: + cp = loop_cp + sam_presets: dict[str, PretenseSam] = {} + for sam_name in [ + "sa2", + "sa3", + "sa5", + "sa6", + "sa10", + "sa11", + "hawk", + "patriot", + "nasamsb", + "nasamsc", + "rapier", + "roland", + "hq7", + "irondome", + "davidsling", + ]: + sam_presets[sam_name] = PretenseSam(sam_name) + + lua_string_zones += " presets.upgrades.supply.fuelTank:extend({\n" + lua_string_zones += ( + " name = '" + + cp_name_trimmed + + "-fueltank-" + + cp_side_str + + "',\n" + ) + lua_string_zones += " products = {\n" + for ground_group in self.game.pretense_ground_supply[cp_side][cp_name_trimmed]: + lua_string_zones += ( + " presets.missions.supply.convoy:extend({ name='" + + ground_group + + "'}),\n" + ) + for ground_group in self.game.pretense_ground_assault[cp_side][cp_name_trimmed]: + lua_string_zones += ( + " presets.missions.attack.surface:extend({ name='" + + ground_group + + "'}),\n" + ) + for mission_type in self.game.pretense_air[cp_side][cp_name_trimmed]: + if mission_type == FlightType.AIR_ASSAULT: + mission_name = "supply.helo" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "'}),\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + lua_string_zones += " presets.upgrades.airdef.bunker:extend({\n" + lua_string_zones += ( + f" name = '{cp_name_trimmed}-shorad-command-" + + cp_side_str + + "',\n" + ) + lua_string_zones += " products = {\n" + lua_string_zones += ( + " presets.defenses." + + cp_side_str + + ".shorad:extend({ name='" + + cp_name_trimmed + + "-shorad-" + + cp_side_str + + "' }),\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + + for ground_object in cp.ground_objects: + for ground_unit in ground_object.units: + if ground_unit.unit_type is not None: + if ground_unit.unit_type.dcs_unit_type == AirDefence.S_75M_Volhov: + sam_presets["sa2"].enabled = True + if ( + ground_unit.unit_type.dcs_unit_type + == AirDefence.x_5p73_s_125_ln + ): + sam_presets["sa3"].enabled = True + if ground_unit.unit_type.dcs_unit_type == AirDefence.S_200_Launcher: + sam_presets["sa5"].enabled = True + if ground_unit.unit_type.dcs_unit_type == AirDefence.Kub_2P25_ln: + sam_presets["sa6"].enabled = True + if ( + ground_unit.unit_type.dcs_unit_type + == AirDefence.S_300PS_5P85C_ln + or ground_unit.unit_type.dcs_unit_type + == AirDefence.S_300PS_5P85D_ln + ): + sam_presets["sa10"].enabled = True + if ( + ground_unit.unit_type.dcs_unit_type + == AirDefence.SA_11_Buk_LN_9A310M1 + ): + sam_presets["sa11"].enabled = True + if ground_unit.unit_type.dcs_unit_type == AirDefence.Hawk_ln: + sam_presets["hawk"].enabled = True + if ground_unit.unit_type.dcs_unit_type == AirDefence.Patriot_ln: + sam_presets["patriot"].enabled = True + if ground_unit.unit_type.dcs_unit_type == AirDefence.NASAMS_LN_B: + sam_presets["nasamsb"].enabled = True + if ground_unit.unit_type.dcs_unit_type == AirDefence.NASAMS_LN_C: + sam_presets["nasamsc"].enabled = True + if ( + ground_unit.unit_type.dcs_unit_type + == AirDefence.rapier_fsa_launcher + ): + sam_presets["rapier"].enabled = True + if ground_unit.unit_type.dcs_unit_type == AirDefence.Roland_ADS: + sam_presets["roland"].enabled = True + if ground_unit.unit_type.dcs_unit_type == AirDefence.HQ_7_STR_SP: + sam_presets["hq7"].enabled = True + if ground_unit.unit_type.dcs_unit_type == IRON_DOME_LN: + sam_presets["irondome"].enabled = True + if ground_unit.unit_type.dcs_unit_type == DAVID_SLING_LN: + sam_presets["davidsling"].enabled = True + + cp_has_sams = False + for sam_name in sam_presets: + if sam_presets[sam_name].enabled: + cp_has_sams = True + break + if cp_has_sams: + lua_string_zones += " presets.upgrades.airdef.comCenter:extend({\n" + lua_string_zones += ( + f" name = '{cp_name_trimmed}-sam-command-" + + cp_side_str + + "',\n" + ) + lua_string_zones += " products = {\n" + for sam_name in sam_presets: + if sam_presets[sam_name].enabled: + lua_string_zones += self.generate_sam_from_preset( + sam_name, cp_side_str, cp_name_trimmed + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + + lua_string_zones += " presets.upgrades.supply.hangar:extend({\n" + lua_string_zones += ( + f" name = '{cp_name_trimmed}-aircraft-command-" + + cp_side_str + + "',\n" + ) + lua_string_zones += " products = {\n" + for mission_type in self.game.pretense_air[cp_side][cp_name_trimmed]: + if mission_type in (FlightType.SEAD, FlightType.DEAD): + mission_name = "attack.sead" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', altitude=25000, expend=AI.Task.WeaponExpend.ALL}),\n" + ) + elif mission_type == FlightType.CAS: + mission_name = "attack.cas" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + flight = self.game.pretense_air_groups[air_group] + if flight.is_helo: + mission_name = "attack.helo" + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', altitude=15000, expend=AI.Task.WeaponExpend.QUARTER}),\n" + ) + elif mission_type == FlightType.BAI: + mission_name = "attack.bai" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', altitude=10000, expend=AI.Task.WeaponExpend.QUARTER}),\n" + ) + elif mission_type == FlightType.STRIKE: + mission_name = "attack.strike" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', altitude=20000, expend=AI.Task.WeaponExpend.ALL}),\n" + ) + elif mission_type == FlightType.BARCAP: + mission_name = "patrol.aircraft" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', altitude=25000, range=25}),\n" + ) + elif mission_type == FlightType.REFUELING: + mission_name = "support.tanker" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + tanker_freq = 257.0 + tanker_tacan = 37.0 + tanker_variant = "Drogue" + for tanker in self.mission_data.tankers: + if tanker.group_name == air_group: + tanker_freq = tanker.freq.hertz / 1000000 + tanker_tacan = tanker.tacan.number if tanker.tacan else 0.0 + if tanker.variant == "KC-135 Stratotanker": + tanker_variant = "Boom" + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', freq='" + + str(tanker_freq) + + "', tacan='" + + str(tanker_tacan) + + "', variant='" + + tanker_variant + + "'}),\n" + ) + elif mission_type == FlightType.AEWC: + mission_name = "support.awacs" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + awacs_freq = 257.5 + for awacs in self.mission_data.awacs: + if awacs.group_name == air_group: + awacs_freq = awacs.freq.hertz / 1000000 + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', freq=" + + str(awacs_freq) + + "}),\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " })\n" + + return lua_string_zones + + def generate_pretense_sea_upgrade_supply(self, cp_name: str, cp_side: int) -> str: + lua_string_zones = "" + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp_name) + cp_side_str = "blue" if cp_side == PRETENSE_BLUE_SIDE else "red" + + supply_ship = "oilPump" + tanker_ship = "chemTank" + command_ship = "comCenter" + + lua_string_zones += ( + " presets.upgrades.supply." + supply_ship + ":extend({\n" + ) + lua_string_zones += ( + " name = '" + + cp_name_trimmed + + f"-{supply_ship}-" + + cp_side_str + + "',\n" + ) + lua_string_zones += " products = {\n" + for ground_group in self.game.pretense_ground_supply[cp_side][cp_name_trimmed]: + lua_string_zones += ( + " presets.missions.supply.convoy:extend({ name='" + + ground_group + + "'}),\n" + ) + for ground_group in self.game.pretense_ground_assault[cp_side][cp_name_trimmed]: + lua_string_zones += ( + " presets.missions.attack.surface:extend({ name='" + + ground_group + + "'}),\n" + ) + for mission_type in self.game.pretense_air[cp_side][cp_name_trimmed]: + if mission_type == FlightType.AIR_ASSAULT: + mission_name = "supply.helo" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "'}),\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + lua_string_zones += ( + " presets.upgrades.airdef." + command_ship + ":extend({\n" + ) + lua_string_zones += ( + f" name = '{cp_name_trimmed}-mission-command-" + + cp_side_str + + "',\n" + ) + lua_string_zones += " products = {\n" + lua_string_zones += ( + " presets.defenses." + + cp_side_str + + ".shorad:extend({ name='" + + cp_name_trimmed + + "-shorad-" + + cp_side_str + + "' }),\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + lua_string_zones += ( + " presets.upgrades.attack." + tanker_ship + ":extend({\n" + ) + lua_string_zones += ( + f" name = '{cp_name_trimmed}-aircraft-command-" + + cp_side_str + + "',\n" + ) + lua_string_zones += " products = {\n" + for mission_type in self.game.pretense_air[cp_side][cp_name_trimmed]: + if mission_type == FlightType.SEAD: + mission_name = "attack.sead" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', altitude=25000, expend=AI.Task.WeaponExpend.ALL}),\n" + ) + elif mission_type == FlightType.CAS: + mission_name = "attack.cas" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + flight = self.game.pretense_air_groups[air_group] + if flight.is_helo: + mission_name = "attack.helo" + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', altitude=15000, expend=AI.Task.WeaponExpend.QUARTER}),\n" + ) + elif mission_type == FlightType.BAI: + mission_name = "attack.bai" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', altitude=10000, expend=AI.Task.WeaponExpend.QUARTER}),\n" + ) + elif mission_type == FlightType.STRIKE: + mission_name = "attack.strike" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', altitude=20000, expend=AI.Task.WeaponExpend.ALL}),\n" + ) + elif mission_type == FlightType.BARCAP: + mission_name = "patrol.aircraft" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', altitude=25000, range=25}),\n" + ) + elif mission_type == FlightType.REFUELING: + mission_name = "support.tanker" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + tanker_freq = 257.0 + tanker_tacan = 37.0 + tanker_variant = "Drogue" + for tanker in self.mission_data.tankers: + if tanker.group_name == air_group: + tanker_freq = tanker.freq.hertz / 1000000 + tanker_tacan = tanker.tacan.number if tanker.tacan else 0.0 + if tanker.variant == "KC-135 Stratotanker": + tanker_variant = "Boom" + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', freq='" + + str(tanker_freq) + + "', tacan='" + + str(tanker_tacan) + + "', variant='" + + tanker_variant + + "'}),\n" + ) + elif mission_type == FlightType.AEWC: + mission_name = "support.awacs" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + awacs_freq = 257.5 + for awacs in self.mission_data.awacs: + if awacs.group_name == air_group: + awacs_freq = awacs.freq.hertz / 1000000 + lua_string_zones += ( + f" presets.missions.{mission_name}:extend" + + "({name='" + + air_group + + "', freq=" + + str(awacs_freq) + + "}),\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " })\n" + + return lua_string_zones + + def generate_pretense_zone_land(self, cp_name: str) -> str: + is_artillery_zone = random.choice([True, False]) + + lua_string_zones = "" + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp_name) + + lua_string_zones += f"zones.{cp_name_trimmed}:defineUpgrades(" + "{\n" + lua_string_zones += " [1] = { --red side\n" + lua_string_zones += " presets.upgrades.basic.tent:extend({\n" + lua_string_zones += f" name='{cp_name_trimmed}-tent-red',\n" + lua_string_zones += " products = {\n" + if not is_artillery_zone: + lua_string_zones += ( + " presets.special.red.infantry:extend({ name='" + + cp_name_trimmed + + "-defense-red'})\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + lua_string_zones += " presets.upgrades.basic.comPost:extend({\n" + lua_string_zones += f" name = '{cp_name_trimmed}-com-red',\n" + lua_string_zones += " products = {\n" + lua_string_zones += ( + " presets.special.red.infantry:extend({ name='" + + cp_name_trimmed + + "-defense-red'}),\n" + ) + if not is_artillery_zone: + lua_string_zones += ( + " presets.defenses.red.infantry:extend({ name='" + + cp_name_trimmed + + "-garrison-red' })\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + if is_artillery_zone: + lua_string_zones += " presets.upgrades.basic.artyBunker:extend({\n" + lua_string_zones += f" name='{cp_name_trimmed}-arty-red',\n" + lua_string_zones += " products = {\n" + lua_string_zones += ( + " presets.defenses.red.artillery:extend({ name='" + + cp_name_trimmed + + "-artillery-red'})\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + + lua_string_zones += self.generate_pretense_land_upgrade_supply( + cp_name, PRETENSE_RED_SIDE + ) + + lua_string_zones += " },\n" + lua_string_zones += " [2] = --blue side\n" + lua_string_zones += " {\n" + lua_string_zones += " presets.upgrades.basic.tent:extend({\n" + lua_string_zones += f" name='{cp_name_trimmed}-tent-blue',\n" + lua_string_zones += " products = {\n" + if not is_artillery_zone: + lua_string_zones += ( + " presets.special.blue.infantry:extend({ name='" + + cp_name_trimmed + + "-defense-blue'})\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + lua_string_zones += " presets.upgrades.basic.comPost:extend({\n" + lua_string_zones += f" name = '{cp_name_trimmed}-com-blue',\n" + lua_string_zones += " products = {\n" + lua_string_zones += ( + " presets.special.blue.infantry:extend({ name='" + + cp_name_trimmed + + "-defense-blue'}),\n" + ) + if not is_artillery_zone: + lua_string_zones += ( + " presets.defenses.blue.infantry:extend({ name='" + + cp_name_trimmed + + "-garrison-blue' })\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + if is_artillery_zone: + lua_string_zones += " presets.upgrades.basic.artyBunker:extend({\n" + lua_string_zones += f" name='{cp_name_trimmed}-arty-blue',\n" + lua_string_zones += " products = {\n" + lua_string_zones += ( + " presets.defenses.blue.artillery:extend({ name='" + + cp_name_trimmed + + "-artillery-blue'})\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" + + lua_string_zones += self.generate_pretense_land_upgrade_supply( + cp_name, PRETENSE_BLUE_SIDE + ) + + lua_string_zones += " }\n" + lua_string_zones += "})\n" + + return lua_string_zones + + def generate_pretense_zone_sea(self, cp_name: str) -> str: + lua_string_zones = "" + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp_name) + + lua_string_zones += f"zones.{cp_name_trimmed}:defineUpgrades(" + "{\n" + lua_string_zones += " [1] = { --red side\n" + + lua_string_zones += self.generate_pretense_sea_upgrade_supply( + cp_name, PRETENSE_RED_SIDE + ) + + lua_string_zones += " },\n" + lua_string_zones += " [2] = --blue side\n" + lua_string_zones += " {\n" + + lua_string_zones += self.generate_pretense_sea_upgrade_supply( + cp_name, PRETENSE_BLUE_SIDE + ) + + lua_string_zones += " }\n" + lua_string_zones += "})\n" + + return lua_string_zones + + def generate_pretense_carrier_zones(self) -> str: + lua_string_carrier_zones = "cmap1 = CarrierMap:new({" + for zone_name in self.game.pretense_carrier_zones: + lua_string_carrier_zones += f'"{zone_name}",' + lua_string_carrier_zones += "})\n" + + return lua_string_carrier_zones + + def generate_pretense_carriers( + self, + cp_name: str, + cp_side: int, + cp_carrier_group_type: Type[ShipType] | None, + cp_carrier_group_name: str | None, + ) -> str: + lua_string_carrier = "\n" + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp_name) + + link4carriers = [Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal] + is_link4carrier = False + carrier_unit_name = "" + icls_channel = 10 + link4_freq = 339000000 + tacan_channel = 44 + tacan_callsign = "" + radio = 137500000 + if cp_carrier_group_type is not None: + if cp_carrier_group_type in link4carriers: + is_link4carrier = True + else: + return lua_string_carrier + if cp_carrier_group_name is None: + return lua_string_carrier + + for carrier in self.mission_data.carriers: + if cp_carrier_group_name == carrier.group_name: + carrier_unit_name = carrier.unit_name + tacan_channel = carrier.tacan.number + tacan_callsign = carrier.callsign + radio = carrier.freq.hertz + if carrier.link4_freq is not None: + link4_freq = carrier.link4_freq.hertz + if carrier.icls_channel is not None: + icls_channel = carrier.icls_channel + break + + lua_string_carrier += ( + f'{cp_name_trimmed} = CarrierCommand:new("' + + carrier_unit_name + + '", 3000, cmap1:getNavMap(), ' + + "{\n" + ) + if is_link4carrier: + lua_string_carrier += " icls = " + str(icls_channel) + ",\n" + lua_string_carrier += " acls = true,\n" + lua_string_carrier += " link4 = " + str(link4_freq) + ",\n" + lua_string_carrier += ( + " tacan = {channel = " + + str(tacan_channel) + + ', callsign="' + + tacan_callsign + + '"},\n' + ) + lua_string_carrier += " radio = " + str(radio) + "\n" + lua_string_carrier += "}, 30000)\n" + + for mission_type in self.game.pretense_air[cp_side][cp_name_trimmed]: + if mission_type == FlightType.SEAD: + mission_name = "supportTypes.strike" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 1000, CarrierCommand.{mission_name}, ' + + "{altitude = 25000, expend=AI.Task.WeaponExpend.ALL})\n" + ) + elif mission_type == FlightType.CAS: + mission_name = "supportTypes.strike" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 1000, CarrierCommand.{mission_name}, ' + + "{altitude = 15000, expend=AI.Task.WeaponExpend.QUARTER})\n" + ) + elif mission_type == FlightType.BAI: + mission_name = "supportTypes.strike" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 1000, CarrierCommand.{mission_name}, ' + + "{altitude = 10000, expend=AI.Task.WeaponExpend.QUARTER})\n" + ) + elif mission_type == FlightType.STRIKE: + mission_name = "supportTypes.strike" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 2000, CarrierCommand.{mission_name}, ' + + "{altitude = 20000})\n" + ) + elif mission_type == FlightType.BARCAP: + mission_name = "supportTypes.cap" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 1000, CarrierCommand.{mission_name}, ' + + "{altitude = 25000, range=25})\n" + ) + elif mission_type == FlightType.REFUELING: + mission_name = "supportTypes.tanker" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + tanker_freq = 257.0 + tanker_tacan = 37.0 + tanker_variant = "Drogue" + for tanker in self.mission_data.tankers: + if tanker.group_name == air_group: + tanker_freq = tanker.freq.hertz / 1000000 + tanker_tacan = tanker.tacan.number if tanker.tacan else 0.0 + if tanker.variant == "KC-135 Stratotanker": + tanker_variant = "Boom" + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 3000, CarrierCommand.{mission_name}, ' + + "{altitude = 19000, freq=" + + str(tanker_freq) + + ", tacan=" + + str(tanker_tacan) + + "})\n" + ) + elif mission_type == FlightType.AEWC: + mission_name = "supportTypes.awacs" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + awacs_freq = 257.5 + for awacs in self.mission_data.awacs: + if awacs.group_name == air_group: + awacs_freq = awacs.freq.hertz / 1000000 + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 5000, CarrierCommand.{mission_name}, ' + + "{altitude = 30000, freq=" + + str(awacs_freq) + + "})\n" + ) + + # lua_string_carrier += f'{cp_name_trimmed}:addExtraSupport("BGM-109B", 10000, CarrierCommand.supportTypes.mslstrike, ' + '{salvo = 10, wpType = \'weapons.missiles.BGM_109B\'})\n' + + return lua_string_carrier + + def get_ground_unit( + self, coalition: Coalition, side: int, desired_unit_classes: list[UnitClass] + ) -> str: + ammo_trucks: List[Type[VehicleType]] = [ + Unarmed.S_75_ZIL, + Unarmed.GAZ_3308, + Unarmed.GAZ_66, + Unarmed.KAMAZ_Truck, + Unarmed.KrAZ6322, + Unarmed.Ural_375, + Unarmed.Ural_375_PBU, + Unarmed.Ural_4320_31, + Unarmed.Ural_4320T, + Unarmed.ZIL_135, + Unarmed.Blitz_36_6700A, + Unarmed.M_818, + Unarmed.Bedford_MWD, + ] + + for unit_class in desired_unit_classes: + if coalition.faction.has_access_to_unit_class(unit_class): + dcs_unit_type = PretenseGroundObjectGenerator.ground_unit_of_class( + coalition=coalition, unit_class=unit_class + ) + if ( + dcs_unit_type is not None + and unit_class == UnitClass.LOGISTICS + and dcs_unit_type.dcs_unit_type.__class__ not in ammo_trucks + ): + # ground_unit_of_class returned a logistics unit not capable of ammo resupply + # Retry up to 10 times + for truck_retry in range(10): + dcs_unit_type = ( + PretenseGroundObjectGenerator.ground_unit_of_class( + coalition=coalition, unit_class=unit_class + ) + ) + if ( + dcs_unit_type is not None + and dcs_unit_type.dcs_unit_type in ammo_trucks + ): + break + else: + dcs_unit_type = None + if dcs_unit_type is not None: + return dcs_unit_type.dcs_id + + # Faction did not contain any of the desired unit classes. + # Fall back to defaults. + if desired_unit_classes[0] == UnitClass.TANK: + if side == PRETENSE_BLUE_SIDE: + return "M-1 Abrams" + else: + return "T-90" + elif desired_unit_classes[0] == UnitClass.ATGM: + if side == PRETENSE_BLUE_SIDE: + return "M1134 Stryker ATGM" + else: + return "BTR_D" + elif desired_unit_classes[0] == UnitClass.IFV: + if side == PRETENSE_BLUE_SIDE: + return "M1128 Stryker MGS" + else: + return "BMP-3" + elif desired_unit_classes[0] == UnitClass.APC: + if side == PRETENSE_BLUE_SIDE: + return "LAV-25" + else: + return "BTR-80" + elif desired_unit_classes[0] == UnitClass.ARTILLERY: + if side == PRETENSE_BLUE_SIDE: + return "M-109" + else: + return "SAU Gvozdika" + elif desired_unit_classes[0] == UnitClass.RECON: + if side == PRETENSE_BLUE_SIDE: + return "M1043 HMMWV Armament" + else: + return "BRDM-2" + elif desired_unit_classes[0] == UnitClass.SHORAD: + if side == PRETENSE_BLUE_SIDE: + return "Roland ADS" + else: + return "2S6 Tunguska" + elif desired_unit_classes[0] == UnitClass.AAA: + if side == PRETENSE_BLUE_SIDE: + return "bofors40" + else: + return "KS-19" + elif desired_unit_classes[0] == UnitClass.MANPAD: + if coalition.game.date.year >= 1990: + if side == PRETENSE_BLUE_SIDE: + return "Soldier stinger" + else: + return "SA-18 Igla manpad" + else: + if side == PRETENSE_BLUE_SIDE: + return "Soldier M4" + else: + return "Infantry AK" + elif desired_unit_classes[0] == UnitClass.LOGISTICS: + if side == PRETENSE_BLUE_SIDE: + return "M 818" + else: + return "Ural-4320T" + else: + if side == PRETENSE_BLUE_SIDE: + return "Soldier M4" + else: + return "Infantry AK" + + def generate_pretense_ground_groups(self, side: int) -> str: + if side == PRETENSE_BLUE_SIDE: + side_str = "blue" + skill_str = self.game.settings.player_skill + coalition = self.game.blue + else: + side_str = "red" + skill_str = self.game.settings.enemy_vehicle_skill + coalition = self.game.red + + lua_string_ground_groups = "" + + lua_string_ground_groups += ( + 'TemplateDB.templates["infantry-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.IFV, UnitClass.APC, UnitClass.RECON])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.TANK, UnitClass.ATGM, UnitClass.IFV, UnitClass.APC, UnitClass.RECON])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.TANK, UnitClass.ATGM, UnitClass.IFV, UnitClass.APC, UnitClass.RECON])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.MANPAD, UnitClass.INFANTRY])}"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["artillery-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.ARTILLERY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.ARTILLERY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.ARTILLERY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.ARTILLERY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.MANPAD, UnitClass.INFANTRY])}"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["defense-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.MANPAD, UnitClass.INFANTRY])}"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["shorad-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += 'TemplateDB.templates["sa2-' + side_str + '"] = {\n' + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "p-19 s-125 sr",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += ' "S_75M_Volhov",\n' + lua_string_ground_groups += ' "S_75M_Volhov",\n' + lua_string_ground_groups += ' "S_75M_Volhov",\n' + lua_string_ground_groups += ' "S_75M_Volhov",\n' + lua_string_ground_groups += ' "S_75M_Volhov",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += ' "RD_75",\n' + lua_string_ground_groups += ' "SNR_75V"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["hawk-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "Hawk pcp",\n' + lua_string_ground_groups += ' "Hawk cwar",\n' + lua_string_ground_groups += ' "Hawk ln",\n' + lua_string_ground_groups += ' "Hawk ln",\n' + lua_string_ground_groups += ' "Hawk ln",\n' + lua_string_ground_groups += ' "Hawk ln",\n' + lua_string_ground_groups += ' "Hawk ln",\n' + lua_string_ground_groups += ' "Hawk tr",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += ' "Hawk sr"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["patriot-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "Patriot cp",\n' + lua_string_ground_groups += ' "Patriot str",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += ' "Patriot ln",\n' + lua_string_ground_groups += ' "Patriot ln",\n' + lua_string_ground_groups += ' "Patriot ln",\n' + lua_string_ground_groups += ' "Patriot ln",\n' + lua_string_ground_groups += ' "Patriot str",\n' + lua_string_ground_groups += ' "Patriot str",\n' + lua_string_ground_groups += ' "Patriot str",\n' + lua_string_ground_groups += ' "Patriot EPP",\n' + lua_string_ground_groups += ' "Patriot ECS",\n' + lua_string_ground_groups += ' "Patriot AMG"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += 'TemplateDB.templates["sa3-' + side_str + '"] = {\n' + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "p-19 s-125 sr",\n' + lua_string_ground_groups += ' "snr s-125 tr",\n' + lua_string_ground_groups += ' "5p73 s-125 ln",\n' + lua_string_ground_groups += ' "5p73 s-125 ln",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += ' "5p73 s-125 ln",\n' + lua_string_ground_groups += ' "5p73 s-125 ln"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += 'TemplateDB.templates["sa6-' + side_str + '"] = {\n' + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "Kub 1S91 str",\n' + lua_string_ground_groups += ' "Kub 2P25 ln",\n' + lua_string_ground_groups += ' "Kub 2P25 ln",\n' + lua_string_ground_groups += ' "Kub 2P25 ln",\n' + lua_string_ground_groups += ' "Kub 2P25 ln",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += ' "Kub 2P25 ln"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}" + + lua_string_ground_groups += ( + 'TemplateDB.templates["sa10-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "S-300PS 54K6 cp",\n' + lua_string_ground_groups += ' "S-300PS 5P85C ln",\n' + lua_string_ground_groups += ' "S-300PS 5P85C ln",\n' + lua_string_ground_groups += ' "S-300PS 5P85C ln",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += ' "S-300PS 5P85C ln",\n' + lua_string_ground_groups += ' "S-300PS 5P85C ln",\n' + lua_string_ground_groups += ' "S-300PS 5P85C ln",\n' + lua_string_ground_groups += ' "S-300PS 40B6MD sr",\n' + lua_string_ground_groups += ' "S-300PS 40B6M tr",\n' + lua_string_ground_groups += ' "S-300PS 64H6E sr"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += 'TemplateDB.templates["sa5-' + side_str + '"] = {\n' + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "RLS_19J6",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += ' "RPC_5N62V",\n' + lua_string_ground_groups += ' "S-200_Launcher",\n' + lua_string_ground_groups += ' "S-200_Launcher",\n' + lua_string_ground_groups += ' "S-200_Launcher",\n' + lua_string_ground_groups += ' "S-200_Launcher",\n' + lua_string_ground_groups += ' "S-200_Launcher",\n' + lua_string_ground_groups += ' "S-200_Launcher"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["sa11-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "SA-11 Buk SR 9S18M1",\n' + lua_string_ground_groups += ' "SA-11 Buk LN 9A310M1",\n' + lua_string_ground_groups += ' "SA-11 Buk LN 9A310M1",\n' + lua_string_ground_groups += ' "SA-11 Buk LN 9A310M1",\n' + lua_string_ground_groups += ' "SA-11 Buk LN 9A310M1",\n' + lua_string_ground_groups += ' "SA-11 Buk LN 9A310M1",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += ' "SA-11 Buk SR 9S18M1",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += ' "SA-11 Buk CC 9S470M1"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["nasamsb-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "NASAMS_Command_Post",\n' + lua_string_ground_groups += ' "NASAMS_Radar_MPQ64F1",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += ' "NASAMS_LN_B",\n' + lua_string_ground_groups += ' "NASAMS_LN_B",\n' + lua_string_ground_groups += ' "NASAMS_LN_B",\n' + lua_string_ground_groups += ' "NASAMS_LN_B",\n' + lua_string_ground_groups += ' "NASAMS_Radar_MPQ64F1",\n' + lua_string_ground_groups += ' "NASAMS_Radar_MPQ64F1",\n' + lua_string_ground_groups += ' "NASAMS_Radar_MPQ64F1"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["nasamsc-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "NASAMS_Command_Post",\n' + lua_string_ground_groups += ' "NASAMS_Radar_MPQ64F1",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += ' "NASAMS_LN_C",\n' + lua_string_ground_groups += ' "NASAMS_LN_C",\n' + lua_string_ground_groups += ' "NASAMS_LN_C",\n' + lua_string_ground_groups += ' "NASAMS_LN_C",\n' + lua_string_ground_groups += ' "NASAMS_Radar_MPQ64F1",\n' + lua_string_ground_groups += ' "NASAMS_Radar_MPQ64F1",\n' + lua_string_ground_groups += ' "NASAMS_Radar_MPQ64F1"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["rapier-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "rapier_fsa_blindfire_radar",\n' + lua_string_ground_groups += ' "rapier_fsa_blindfire_radar",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += ' "rapier_fsa_launcher",\n' + lua_string_ground_groups += ' "rapier_fsa_launcher",\n' + lua_string_ground_groups += ' "rapier_fsa_launcher",\n' + lua_string_ground_groups += ' "rapier_fsa_launcher",\n' + lua_string_ground_groups += ( + ' "rapier_fsa_optical_tracker_unit",\n' + ) + lua_string_ground_groups += ( + ' "rapier_fsa_optical_tracker_unit",\n' + ) + lua_string_ground_groups += ( + ' "rapier_fsa_optical_tracker_unit"\n' + ) + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["roland-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += ' "Roland Radar",\n' + lua_string_ground_groups += ' "Roland Radar",\n' + lua_string_ground_groups += ' "Roland Radar"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += 'TemplateDB.templates["hq7-' + side_str + '"] = {\n' + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "HQ-7_LN_EO",\n' + lua_string_ground_groups += ' "HQ-7_LN_EO",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += ' "HQ-7_LN_SP",\n' + lua_string_ground_groups += ' "HQ-7_LN_SP",\n' + lua_string_ground_groups += ' "HQ-7_LN_SP",\n' + lua_string_ground_groups += ' "HQ-7_LN_SP",\n' + lua_string_ground_groups += ' "HQ-7_STR_SP",\n' + lua_string_ground_groups += ' "HQ-7_STR_SP",\n' + lua_string_ground_groups += ' "HQ-7_STR_SP"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["irondome-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "Iron_Dome_David_Sling_CP",\n' + lua_string_ground_groups += ' "ELM2084_MMR_AD_RT",\n' + lua_string_ground_groups += ' "ELM2084_MMR_AD_SC",\n' + lua_string_ground_groups += ' "ELM2084_MMR_WLR",\n' + lua_string_ground_groups += ' "IRON_DOME_LN",\n' + lua_string_ground_groups += ' "IRON_DOME_LN",\n' + lua_string_ground_groups += ' "IRON_DOME_LN",\n' + lua_string_ground_groups += ' "IRON_DOME_LN",\n' + lua_string_ground_groups += ' "IRON_DOME_LN",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + lua_string_ground_groups += ( + 'TemplateDB.templates["davidsling-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "Iron_Dome_David_Sling_CP",\n' + lua_string_ground_groups += ' "ELM2084_MMR_AD_RT",\n' + lua_string_ground_groups += ' "ELM2084_MMR_AD_SC",\n' + lua_string_ground_groups += ' "ELM2084_MMR_WLR",\n' + lua_string_ground_groups += ' "DAVID_SLING_LN",\n' + lua_string_ground_groups += ' "DAVID_SLING_LN",\n' + lua_string_ground_groups += ' "DAVID_SLING_LN",\n' + lua_string_ground_groups += ' "DAVID_SLING_LN",\n' + lua_string_ground_groups += ' "DAVID_SLING_LN",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + + return lua_string_ground_groups + + @staticmethod + def generate_pretense_zone_connection( + connected_points: dict[str, list[str]], + cp_name: str, + other_cp_name: str, + ) -> str: + lua_string_connman = "" + try: + connected_points[cp_name] + except KeyError: + connected_points[cp_name] = list() + try: + connected_points[other_cp_name] + except KeyError: + connected_points[other_cp_name] = list() + + if ( + other_cp_name not in connected_points[cp_name] + and cp_name not in connected_points[other_cp_name] + ): + cp_name_conn = "".join( + [i for i in cp_name if i.isalnum() or i.isspace() or i == "-"] + ) + cp_name_conn_other = "".join( + [i for i in other_cp_name if i.isalnum() or i.isspace() or i == "-"] + ) + cp_name_conn = cp_name_conn.replace("Ä", "A") + cp_name_conn = cp_name_conn.replace("Ö", "O") + cp_name_conn = cp_name_conn.replace("Ø", "O") + cp_name_conn = cp_name_conn.replace("ä", "a") + cp_name_conn = cp_name_conn.replace("ö", "o") + cp_name_conn = cp_name_conn.replace("ø", "o") + + cp_name_conn_other = cp_name_conn_other.replace("Ä", "A") + cp_name_conn_other = cp_name_conn_other.replace("Ö", "O") + cp_name_conn_other = cp_name_conn_other.replace("Ø", "O") + cp_name_conn_other = cp_name_conn_other.replace("ä", "a") + cp_name_conn_other = cp_name_conn_other.replace("ö", "o") + cp_name_conn_other = cp_name_conn_other.replace("ø", "o") + lua_string_connman = ( + f" cm: addConnection('{cp_name_conn}', '{cp_name_conn_other}')\n" + ) + connected_points[cp_name].append(other_cp_name) + connected_points[other_cp_name].append(cp_name) + + return lua_string_connman + + def generate_pretense_plugin_data(self) -> None: + self.inject_plugin_script("base", "mist_4_5_126.lua", "mist_4_5_126") + + lua_string_config = "Config = Config or {}\n" + + lua_string_config += ( + f"Config.maxDistFromFront = " + + str(self.game.settings.pretense_maxdistfromfront_distance * 1000) + + "\n" + ) + trigger = TriggerStart(comment="Pretense config") + trigger.add_action(DoScript(String(lua_string_config))) + self.mission.triggerrules.triggers.append(trigger) + + self.inject_plugin_script( + "pretense", "pretense_compiled.lua", "pretense_compiled" + ) + + trigger = TriggerStart(comment="Pretense init") + + now = datetime.now() + date_time = now.strftime("%Y-%m-%dT%H_%M_%S") + lua_string_savefile = ( + f"local savefile = 'pretense_retribution_{date_time}.json'" + ) + + init_header_file = open("./resources/plugins/pretense/init_header.lua", "r") + init_header = init_header_file.read() + + lua_string_ground_groups_blue = self.generate_pretense_ground_groups( + PRETENSE_BLUE_SIDE + ) + lua_string_ground_groups_red = self.generate_pretense_ground_groups( + PRETENSE_RED_SIDE + ) + + lua_string_zones = "" + lua_string_carriers = "" + if self.game.settings.pretense_controllable_carrier: + lua_string_carriers += self.generate_pretense_carrier_zones() + + for cp in self.game.theater.controlpoints: + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp.name) + cp_name = "".join( + [i for i in cp.name if i.isalnum() or i.isspace() or i == "-"] + ) + cp_name.replace("Ä", "A") + cp_name.replace("Ö", "O") + cp_name.replace("Ø", "O") + cp_name.replace("ä", "a") + cp_name.replace("ö", "o") + cp_name.replace("ø", "o") + cp_side = 2 if cp.captured else 1 + + if isinstance(cp, OffMapSpawn): + continue + elif ( + cp.is_fleet + and cp.captured + and self.game.settings.pretense_controllable_carrier + ): + # Friendly carrier, generate carrier parameters + cp_carrier_group_type = cp.get_carrier_group_type() + cp_carrier_group_name = cp.get_carrier_group_name() + lua_string_carriers += self.generate_pretense_carriers( + cp_name, cp_side, cp_carrier_group_type, cp_carrier_group_name + ) + continue + + for side in range(1, 3): + if cp_name_trimmed not in self.game.pretense_air[cp_side]: + self.game.pretense_air[side][cp_name_trimmed] = {} + if cp_name_trimmed not in self.game.pretense_ground_supply[cp_side]: + self.game.pretense_ground_supply[side][cp_name_trimmed] = list() + if cp_name_trimmed not in self.game.pretense_ground_assault[cp_side]: + self.game.pretense_ground_assault[side][cp_name_trimmed] = list() + cp_name = cp_name.replace("Ä", "A") + cp_name = cp_name.replace("Ö", "O") + cp_name = cp_name.replace("Ø", "O") + cp_name = cp_name.replace("ä", "a") + cp_name = cp_name.replace("ö", "o") + cp_name = cp_name.replace("ø", "o") + lua_string_zones += ( + f"zones.{cp_name_trimmed} = ZoneCommand:new('{cp_name}')\n" + ) + lua_string_zones += ( + f"zones.{cp_name_trimmed}.initialState = " + + "{ side=" + + str(cp_side) + + " }\n" + ) + max_resource = 20000 + is_helo_spawn = "false" + is_plane_spawn = "false" + is_keep_active = "false" + if cp.has_helipads: + is_helo_spawn = "true" + max_resource = 30000 + if isinstance(cp, Airfield) or cp.has_ground_spawns: + is_helo_spawn = "true" + is_plane_spawn = "true" + if cp.has_ground_spawns or cp.is_lha: + is_helo_spawn = "true" + is_plane_spawn = "true" + max_resource = 40000 + if cp.is_lha: + is_keep_active = "true" + if isinstance(cp, Airfield) or cp.is_carrier: + is_helo_spawn = "true" + is_plane_spawn = "true" + is_keep_active = "true" + max_resource = 50000 + lua_string_zones += ( + f"zones.{cp_name_trimmed}.maxResource = {max_resource}\n" + ) + lua_string_zones += ( + f"zones.{cp_name_trimmed}.isHeloSpawn = " + is_helo_spawn + "\n" + ) + lua_string_zones += ( + f"zones.{cp_name_trimmed}.isPlaneSpawn = " + is_plane_spawn + "\n" + ) + lua_string_zones += ( + f"zones.{cp_name_trimmed}.keepActive = " + is_keep_active + "\n" + ) + if cp.is_fleet: + lua_string_zones += self.generate_pretense_zone_sea(cp_name) + else: + lua_string_zones += self.generate_pretense_zone_land(cp_name) + + lua_string_connman = " cm = ConnectionManager:new()\n" + + # Generate ConnectionManager connections + connected_points: dict[str, list[str]] = {} + for cp in self.game.theater.controlpoints: + for other_cp in cp.connected_points: + lua_string_connman += self.generate_pretense_zone_connection( + connected_points, cp.name, other_cp.name + ) + for sea_connection in cp.shipping_lanes: + lua_string_connman += self.generate_pretense_zone_connection( + connected_points, + cp.name, + sea_connection.name, + ) + if len(cp.connected_points) == 0 and len(cp.shipping_lanes) == 0: + # Also connect carrier and LHA control points to adjacent friendly points + if cp.is_fleet and ( + not self.game.settings.pretense_controllable_carrier + or not cp.captured + ): + num_of_carrier_connections = 0 + for ( + other_cp + ) in self.game.theater.closest_friendly_control_points_to(cp): + num_of_carrier_connections += 1 + if ( + num_of_carrier_connections + > PRETENSE_NUMBER_OF_ZONES_TO_CONNECT_CARRIERS_TO + ): + break + + lua_string_connman += self.generate_pretense_zone_connection( + connected_points, cp.name, other_cp.name + ) + else: + # Finally, connect remaining non-connected points + closest_cps = self.game.theater.closest_friendly_control_points_to(cp) + for extra_connection in range( + self.game.settings.pretense_extra_zone_connections + ): + try: + if ( + cp.is_fleet + and cp.captured + and self.game.settings.pretense_controllable_carrier + ): + break + elif ( + closest_cps[extra_connection].is_fleet + and closest_cps[extra_connection].captured + and self.game.settings.pretense_controllable_carrier + ): + break + elif len(closest_cps) > extra_connection: + lua_string_connman += ( + self.generate_pretense_zone_connection( + connected_points, + cp.name, + closest_cps[extra_connection].name, + ) + ) + else: + break + except IndexError: + # No more connected points, so no need to continue the loop + break + + lua_string_supply = "local redSupply = {\n" + # Generate supply + for cp_side in range(1, 3): + for cp in self.game.theater.controlpoints: + if isinstance(cp, OffMapSpawn): + continue + cp_side_captured = cp_side == 2 + if cp_side_captured != cp.captured: + continue + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( + cp.name + ) + for mission_type in self.game.pretense_air[cp_side][cp_name_trimmed]: + if mission_type == FlightType.PRETENSE_CARGO: + for air_group in self.game.pretense_air[cp_side][ + cp_name_trimmed + ][mission_type]: + lua_string_supply += f"'{air_group}'," + lua_string_supply += "}\n" + if cp_side < 2: + lua_string_supply += "local blueSupply = {\n" + lua_string_supply += "local offmapZones = {\n" + for cp in self.game.theater.controlpoints: + if isinstance(cp, Airfield): + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( + cp.name + ) + lua_string_supply += f" zones.{cp_name_trimmed},\n" + lua_string_supply += "}\n" + + init_body_1_file = open("./resources/plugins/pretense/init_body_1.lua", "r") + init_body_1 = init_body_1_file.read() + + lua_string_jtac = "" + for jtac in self.mission_data.jtacs: + lua_string_jtac = f"Group.getByName('{jtac.group_name}'): destroy()\n" + lua_string_jtac += ( + "CommandFunctions.jtac = JTAC:new({name = '" + jtac.group_name + "'})\n" + ) + + init_body_2_file = open("./resources/plugins/pretense/init_body_2.lua", "r") + init_body_2 = init_body_2_file.read() + + init_body_3_file = open("./resources/plugins/pretense/init_body_3.lua", "r") + init_body_3 = init_body_3_file.read() + + init_footer_file = open("./resources/plugins/pretense/init_footer.lua", "r") + init_footer = init_footer_file.read() + + lua_string = ( + lua_string_savefile + + init_header + + lua_string_ground_groups_blue + + lua_string_ground_groups_red + + init_body_1 + + lua_string_zones + + lua_string_connman + + init_body_2 + + lua_string_jtac + + lua_string_carriers + + init_body_3 + + lua_string_supply + + init_footer + ) + + trigger.add_action(DoScript(String(lua_string))) + self.mission.triggerrules.triggers.append(trigger) + + file1 = open(Path("./resources/plugins/pretense", "pretense_output.lua"), "w") + file1.write(lua_string) + file1.close() + + def inject_lua_trigger(self, contents: str, comment: str) -> None: + trigger = TriggerStart(comment=comment) + trigger.add_action(DoScript(String(contents))) + self.mission.triggerrules.triggers.append(trigger) + + def bypass_plugin_script(self, mnemonic: str) -> None: + self.plugin_scripts.append(mnemonic) + + def inject_plugin_script( + self, plugin_mnemonic: str, script: str, script_mnemonic: str + ) -> None: + if script_mnemonic in self.plugin_scripts: + logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}") + return + + self.plugin_scripts.append(script_mnemonic) + + plugin_path = Path("./resources/plugins", plugin_mnemonic) + + script_path = Path(plugin_path, script) + if not script_path.exists(): + logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}") + return + + trigger = TriggerStart(comment=f"Load {script_mnemonic}") + filename = script_path.resolve() + fileref = self.mission.map_resource.add_resource_file(filename) + trigger.add_action(DoScriptFile(fileref)) + self.mission.triggerrules.triggers.append(trigger) + + def inject_plugins(self) -> None: + for plugin in LuaPluginManager.plugins(): + if plugin.enabled: + plugin.inject_scripts(self) + plugin.inject_configuration(self) + + +class LuaValue: + key: Optional[str] + value: str | list[str] + + def __init__(self, key: Optional[str], value: str | list[str]): + self.key = key + self.value = value + + def serialize(self) -> str: + serialized_value = self.key + " = " if self.key else "" + if isinstance(self.value, str): + serialized_value += f'"{escape_string_for_lua(self.value)}"' + else: + escaped_values = [f'"{escape_string_for_lua(v)}"' for v in self.value] + serialized_value += "{" + ", ".join(escaped_values) + "}" + return serialized_value + + +class LuaItem(ABC): + value: LuaValue | list[LuaValue] + name: Optional[str] + + def __init__(self, name: Optional[str]): + self.value = [] + self.name = name + + def set_value(self, value: str) -> None: + self.value = LuaValue(None, value) + + def set_data_array(self, values: list[str]) -> None: + self.value = LuaValue(None, values) + + def add_data_array(self, key: str, values: list[str]) -> None: + self._add_value(LuaValue(key, values)) + + def add_key_value(self, key: str, value: str) -> None: + self._add_value(LuaValue(key, value)) + + def _add_value(self, value: LuaValue) -> None: + if isinstance(self.value, list): + self.value.append(value) + else: + self.value = value + + @abstractmethod + def add_item(self, item_name: Optional[str] = None) -> LuaItem: + """adds a new item to the LuaArray without checking the existence""" + raise NotImplementedError + + @abstractmethod + def get_item(self, item_name: str) -> Optional[LuaItem]: + """gets item from LuaArray. Returns None if it does not exist""" + raise NotImplementedError + + @abstractmethod + def get_or_create_item(self, item_name: Optional[str] = None) -> LuaItem: + """gets item from the LuaArray or creates one if it does not exist already""" + raise NotImplementedError + + @abstractmethod + def serialize(self) -> str: + if isinstance(self.value, LuaValue): + return self.value.serialize() + else: + serialized_data = [d.serialize() for d in self.value] + return "{" + ", ".join(serialized_data) + "}" + + +class LuaData(LuaItem): + objects: list[LuaData] + base_name: Optional[str] + + def __init__(self, name: Optional[str], is_base_name: bool = True): + self.objects = [] + self.base_name = name if is_base_name else None + super().__init__(name) + + def add_item(self, item_name: Optional[str] = None) -> LuaItem: + item = LuaData(item_name, False) + self.objects.append(item) + return item + + def get_item(self, item_name: str) -> Optional[LuaItem]: + for lua_object in self.objects: + if lua_object.name == item_name: + return lua_object + return None + + def get_or_create_item(self, item_name: Optional[str] = None) -> LuaItem: + if item_name: + item = self.get_item(item_name) + if item: + return item + return self.add_item(item_name) + + def serialize(self, level: int = 0) -> str: + """serialize the LuaData to a string""" + serialized_data: list[str] = [] + serialized_name = "" + linebreak = "\n" + tab = "\t" + tab_end = "" + for _ in range(level): + tab += "\t" + tab_end += "\t" + if self.base_name: + # Only used for initialization of the object in lua + serialized_name += self.base_name + " = " + if self.objects: + # nested objects + serialized_objects = [o.serialize(level + 1) for o in self.objects] + if self.name: + if self.name is not self.base_name: + serialized_name += self.name + " = " + serialized_data.append( + serialized_name + + "{" + + linebreak + + tab + + ("," + linebreak + tab).join(serialized_objects) + + linebreak + + tab_end + + "}" + ) + else: + # key with value + if self.name: + serialized_data.append(self.name + " = " + super().serialize()) + # only value + else: + serialized_data.append(super().serialize()) + + return "\n".join(serialized_data) + + def create_operations_lua(self) -> str: + """crates the liberation lua script for the dcs mission""" + lua_prefix = """ +-- setting configuration table +env.info("DCSRetribution|: setting configuration table") +""" + + return lua_prefix + self.serialize() diff --git a/game/pretense/pretensemissiongenerator.py b/game/pretense/pretensemissiongenerator.py new file mode 100644 index 00000000..8cc693fc --- /dev/null +++ b/game/pretense/pretensemissiongenerator.py @@ -0,0 +1,296 @@ +from __future__ import annotations + +import logging +import pickle +from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING + +import dcs.lua +from dcs import Mission, Point +from dcs.coalition import Coalition +from dcs.countries import ( + country_dict, + CombinedJointTaskForcesBlue, + CombinedJointTaskForcesRed, +) +from dcs.task import AFAC, FAC, SetInvisibleCommand, SetImmortalCommand, OrbitAction + +from game.lasercodes.lasercoderegistry import LaserCodeRegistry +from game.missiongenerator.convoygenerator import ConvoyGenerator +from game.missiongenerator.environmentgenerator import EnvironmentGenerator +from game.missiongenerator.forcedoptionsgenerator import ForcedOptionsGenerator +from game.missiongenerator.frontlineconflictdescription import ( + FrontLineConflictDescription, +) +from game.missiongenerator.missiondata import MissionData, JtacInfo +from game.missiongenerator.tgogenerator import TgoGenerator +from game.missiongenerator.visualsgenerator import VisualsGenerator +from game.naming import namegen +from game.persistency import pre_pretense_backups_dir +from game.pretense.pretenseaircraftgenerator import PretenseAircraftGenerator +from game.radio.radios import RadioRegistry +from game.radio.tacan import TacanRegistry +from game.theater.bullseye import Bullseye +from game.unitmap import UnitMap +from .pretenseluagenerator import PretenseLuaGenerator +from .pretensetgogenerator import PretenseTgoGenerator +from .pretensetriggergenerator import PretenseTriggerGenerator +from ..ato.airtaaskingorder import AirTaskingOrder +from ..callsigns import callsign_for_support_unit +from ..dcs.aircrafttype import AircraftType +from ..missiongenerator import MissionGenerator +from ..theater import Airfield + +if TYPE_CHECKING: + from game import Game + + +class PretenseMissionGenerator(MissionGenerator): + def __init__(self, game: Game, time: datetime) -> None: + super().__init__(game, time) + self.game = game + self.time = time + self.mission = Mission(game.theater.terrain) + self.unit_map = UnitMap() + + self.mission_data = MissionData() + + self.laser_code_registry = LaserCodeRegistry() + self.radio_registry = RadioRegistry() + self.tacan_registry = TacanRegistry() + + self.generation_started = False + + self.p_country = country_dict[self.game.blue.faction.country.id]() + self.e_country = country_dict[self.game.red.faction.country.id]() + + with open("resources/default_options.lua", "r", encoding="utf-8") as f: + options = dcs.lua.loads(f.read())["options"] + ext_view = game.settings.external_views_allowed + options["miscellaneous"]["f11_free_camera"] = ext_view + options["difficulty"]["spectatorExternalViews"] = ext_view + self.mission.options.load_from_dict(options) + + def generate_miz(self, output: Path) -> UnitMap: + game_backup_pickle = pickle.dumps(self.game) + path = pre_pretense_backups_dir() + path.mkdir(parents=True, exist_ok=True) + path /= f".pre-pretense-backup.retribution" + try: + with open(path, "wb") as f: + pickle.dump(self.game, f) + except: + logging.error(f"Unable to save Pretense pre-generation backup to {path}") + + if self.generation_started: + raise RuntimeError( + "Mission has already begun generating. To reset, create a new " + "MissionSimulation." + ) + self.generation_started = True + + self.game.pretense_ground_supply = {1: {}, 2: {}} + self.game.pretense_ground_assault = {1: {}, 2: {}} + self.game.pretense_air = {1: {}, 2: {}} + + self.setup_mission_coalitions() + self.add_airfields_to_unit_map() + self.initialize_registries() + + EnvironmentGenerator(self.mission, self.game.conditions, self.time).generate() + + tgo_generator = PretenseTgoGenerator( + self.mission, + self.game, + self.radio_registry, + self.tacan_registry, + self.unit_map, + self.mission_data, + ) + tgo_generator.generate() + + ConvoyGenerator(self.mission, self.game, self.unit_map).generate() + + # Generate ground conflicts first so the JTACs get the first laser code (1688) + # rather than the first player flight with a TGP. + self.generate_ground_conflicts() + self.generate_air_units(tgo_generator) + + for cp in self.game.theater.controlpoints: + if ( + self.game.settings.ground_start_airbase_statics_farps_remove + and isinstance(cp, Airfield) + ): + while len(tgo_generator.ground_spawns[cp]) > 0: + ground_spawn = tgo_generator.ground_spawns[cp].pop() + # Remove invisible FARPs from airfields because they are unnecessary + neutral_country = self.mission.country( + cp.coalition.game.neutral_country.name + ) + neutral_country.remove_static_group(ground_spawn[0]) + while len(tgo_generator.ground_spawns_roadbase[cp]) > 0: + ground_spawn = tgo_generator.ground_spawns_roadbase[cp].pop() + # Remove invisible FARPs from airfields because they are unnecessary + neutral_country = self.mission.country( + cp.coalition.game.neutral_country.name + ) + neutral_country.remove_static_group(ground_spawn[0]) + + self.mission.triggerrules.triggers.clear() + PretenseTriggerGenerator(self.mission, self.game).generate() + ForcedOptionsGenerator(self.mission, self.game).generate() + VisualsGenerator(self.mission, self.game).generate() + PretenseLuaGenerator(self.game, self.mission, self.mission_data).generate() + + self.setup_combined_arms() + + self.notify_info_generators() + + # TODO: Shouldn't this be first? + namegen.reset_numbers() + self.mission.save(output) + + print( + f"Loading pre-pretense save, number of BLUFOR squadrons: {len(self.game.blue.air_wing.squadrons)}" + ) + self.game = pickle.loads(game_backup_pickle) + print( + f"Loaded pre-pretense save, number of BLUFOR squadrons: {len(self.game.blue.air_wing.squadrons)}" + ) + self.game.on_load() + + return self.unit_map + + def setup_mission_coalitions(self) -> None: + self.mission.coalition["blue"] = Coalition( + "blue", bullseye=self.game.blue.bullseye.to_pydcs() + ) + self.mission.coalition["red"] = Coalition( + "red", bullseye=self.game.red.bullseye.to_pydcs() + ) + self.mission.coalition["neutrals"] = Coalition( + "neutrals", bullseye=Bullseye(Point(0, 0, self.mission.terrain)).to_pydcs() + ) + + self.mission.coalition["blue"].add_country(self.p_country) + self.mission.coalition["red"].add_country(self.e_country) + + # Add CJTF factions to the coalitions, if they're not being used in the campaign + if CombinedJointTaskForcesBlue.id not in {self.p_country.id, self.e_country.id}: + self.mission.coalition["blue"].add_country(CombinedJointTaskForcesBlue()) + if CombinedJointTaskForcesRed.id not in {self.p_country.id, self.e_country.id}: + self.mission.coalition["red"].add_country(CombinedJointTaskForcesRed()) + + belligerents = {self.p_country.id, self.e_country.id} + for country_id in country_dict.keys(): + if country_id not in belligerents: + c = country_dict[country_id]() + self.mission.coalition["neutrals"].add_country(c) + + def generate_ground_conflicts(self) -> None: + """Generate FLOTs and JTACs for each active front line.""" + for front_line in self.game.theater.conflicts(): + player_cp = front_line.blue_cp + enemy_cp = front_line.red_cp + + # Add JTAC + if self.game.blue.faction.has_jtac: + freq = self.radio_registry.alloc_uhf() + # If the option fc3LaserCode is enabled, force all JTAC + # laser codes to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs. + # Otherwise use 1688 for the first JTAC, 1687 for the second etc. + if self.game.settings.plugins.get("ctld.fc3LaserCode"): + code = self.game.laser_code_registry.fc3_code + else: + code = front_line.laser_code + + utype = self.game.blue.faction.jtac_unit + if utype is None: + utype = AircraftType.named("MQ-9 Reaper") + + country = self.mission.country(self.game.blue.faction.country.name) + position = FrontLineConflictDescription.frontline_position( + front_line, self.game.theater, self.game.settings + ) + jtac = self.mission.flight_group( + country=country, + name=namegen.next_jtac_name(), + aircraft_type=utype.dcs_unit_type, + position=position[0], + airport=None, + altitude=5000, + maintask=AFAC, + ) + jtac.points[0].tasks.append( + FAC( + callsign=len(self.mission_data.jtacs) + 1, + frequency=int(freq.mhz), + modulation=freq.modulation, + ) + ) + jtac.points[0].tasks.append(SetInvisibleCommand(True)) + jtac.points[0].tasks.append(SetImmortalCommand(True)) + jtac.points[0].tasks.append( + OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle) + ) + frontline = f"Frontline {player_cp.name}/{enemy_cp.name}" + # Note: Will need to change if we ever add ground based JTAC. + callsign = callsign_for_support_unit(jtac) + self.mission_data.jtacs.append( + JtacInfo( + group_name=jtac.name, + unit_name=jtac.units[0].name, + callsign=callsign, + region=frontline, + code=str(code), + blue=True, + freq=freq, + ) + ) + + def generate_air_units(self, tgo_generator: TgoGenerator) -> None: + """Generate the air units for the Operation""" + + # Generate Aircraft Activity on the map + aircraft_generator = PretenseAircraftGenerator( + self.mission, + self.game.settings, + self.game, + self.time, + self.radio_registry, + self.tacan_registry, + self.laser_code_registry, + self.unit_map, + mission_data=self.mission_data, + helipads=tgo_generator.helipads, + ground_spawns_roadbase=tgo_generator.ground_spawns_roadbase, + ground_spawns_large=tgo_generator.ground_spawns_large, + ground_spawns=tgo_generator.ground_spawns, + ) + + # Clear parking slots and ATOs + aircraft_generator.clear_parking_slots() + self.game.blue.ato.clear() + self.game.red.ato.clear() + + for cp in self.game.theater.controlpoints: + for country in (self.p_country, self.e_country): + ato = AirTaskingOrder() + aircraft_generator.generate_flights( + country, + cp, + ato, + ) + aircraft_generator.generate_packages( + country, + ato, + tgo_generator.runways, + ) + + self.mission_data.flights = aircraft_generator.flights + + for flight in aircraft_generator.flights: + if not flight.client_units: + continue + flight.aircraft_type.assign_channels_for_flight(flight, self.mission_data) diff --git a/game/pretense/pretensetgogenerator.py b/game/pretense/pretensetgogenerator.py new file mode 100644 index 00000000..3d02298c --- /dev/null +++ b/game/pretense/pretensetgogenerator.py @@ -0,0 +1,955 @@ +"""Generators for creating the groups for ground objectives. + +The classes in this file are responsible for creating the vehicle groups, ship +groups, statics, missile sites, and AA sites for the mission. Each of these +objectives is defined in the Theater by a TheaterGroundObject. These classes +create the pydcs groups and statics for those areas and add them to the mission. +""" +from __future__ import annotations + +import random +import logging +from collections import defaultdict +from typing import Dict, Optional, TYPE_CHECKING, Tuple, Type, Iterator + +from dcs import Mission, Point +from dcs.countries import * +from dcs.country import Country +from dcs.ships import Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal, LHA_Tarawa +from dcs.unitgroup import StaticGroup, VehicleGroup +from dcs.unittype import VehicleType + +from game.coalition import Coalition +from game.data.units import UnitClass +from game.dcs.groundunittype import GroundUnitType +from game.missiongenerator.groundforcepainter import ( + GroundForcePainter, +) +from game.missiongenerator.missiondata import MissionData, CarrierInfo +from game.missiongenerator.tgogenerator import ( + TgoGenerator, + HelipadGenerator, + GroundSpawnRoadbaseGenerator, + GroundSpawnGenerator, + GroundObjectGenerator, + CarrierGenerator, + LhaGenerator, + MissileSiteGenerator, + GenericCarrierGenerator, +) +from game.point_with_heading import PointWithHeading +from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator +from game.radio.radios import RadioRegistry +from game.radio.tacan import TacanRegistry, TacanBand, TacanUsage +from game.runways import RunwayData +from game.theater import ( + ControlPoint, + TheaterGroundObject, + TheaterUnit, + NavalControlPoint, + PresetLocation, +) +from game.theater.theatergroundobject import ( + CarrierGroundObject, + LhaGroundObject, + MissileSiteGroundObject, + BuildingGroundObject, + VehicleGroupGroundObject, + GenericCarrierGroundObject, +) +from game.theater.theatergroup import TheaterGroup +from game.unitmap import UnitMap +from game.utils import Heading +from pydcs_extensions import ( + Char_M551_Sheridan, + BV410_RBS70, + BV410_RBS90, + BV410, + VAB__50, + VAB_T20_13, +) + +if TYPE_CHECKING: + from game import Game + +FARP_FRONTLINE_DISTANCE = 10000 +AA_CP_MIN_DISTANCE = 40000 +PRETENSE_GROUND_UNIT_GROUP_SIZE = 5 +PRETENSE_GROUND_UNITS_TO_REMOVE_FROM_ASSAULT = [ + vehicles.Armor.Stug_III, + vehicles.Artillery.Grad_URAL, +] +PRETENSE_AMPHIBIOUS_UNITS = [ + vehicles.Unarmed.LARC_V, + vehicles.Armor.AAV7, + vehicles.Armor.LAV_25, + vehicles.Armor.TPZ, + vehicles.Armor.PT_76, + vehicles.Armor.BMD_1, + vehicles.Armor.BMP_1, + vehicles.Armor.BMP_2, + vehicles.Armor.BMP_3, + vehicles.Armor.BTR_80, + vehicles.Armor.BTR_82A, + vehicles.Armor.BRDM_2, + vehicles.Armor.BTR_D, + vehicles.Armor.MTLB, + vehicles.Armor.ZBD04A, + vehicles.Armor.VAB_Mephisto, + VAB__50, + VAB_T20_13, + Char_M551_Sheridan, + BV410_RBS70, + BV410_RBS90, + BV410, +] + + +class PretenseGroundObjectGenerator(GroundObjectGenerator): + """generates the DCS groups and units from the TheaterGroundObject""" + + def __init__( + self, + ground_object: TheaterGroundObject, + country: Country, + game: Game, + mission: Mission, + unit_map: UnitMap, + ) -> None: + super().__init__( + ground_object, + country, + game, + mission, + unit_map, + ) + + self.ground_object = ground_object + self.country = country + self.game = game + self.m = mission + self.unit_map = unit_map + self.coalition = ground_object.coalition + + @property + def culled(self) -> bool: + return self.game.iads_considerate_culling(self.ground_object) + + @staticmethod + def ground_unit_of_class( + coalition: Coalition, unit_class: UnitClass + ) -> Optional[GroundUnitType]: + """ + Returns a GroundUnitType of the specified class that belongs to the + TheaterGroundObject faction. + + Units, which are known to have pathfinding issues in Pretense missions + are removed based on a pre-defined list. + + Args: + coalition: Coalition to return the unit for. + unit_class: Class of unit to return. + """ + faction_units = ( + set(coalition.faction.frontline_units) + | set(coalition.faction.artillery_units) + | set(coalition.faction.air_defense_units) + | set(coalition.faction.logistics_units) + ) + of_class = list({u for u in faction_units if u.unit_class is unit_class}) + + # Remove units from list with known pathfinding issues in Pretense missions + for unit_to_remove in PRETENSE_GROUND_UNITS_TO_REMOVE_FROM_ASSAULT: + for groundunittype_to_remove in GroundUnitType.for_dcs_type(unit_to_remove): + if groundunittype_to_remove in of_class: + of_class.remove(groundunittype_to_remove) + + if len(of_class) > 0: + return random.choice(of_class) + else: + return None + + def generate_ground_unit_of_class( + self, + unit_class: UnitClass, + group: TheaterGroup, + vehicle_units: list[TheaterUnit], + cp_name: str, + group_role: str, + max_num: int, + ) -> None: + """ + Generates a single land based TheaterUnit for a Pretense unit group + for a specific TheaterGroup, provided that the group still has room + (defined by the max_num argument). Land based groups don't have + restrictions on the unit types, other than that they must be + accessible by the faction and must be of the specified class. + + Generated units are placed 30 meters from the TheaterGroup + position in a random direction. + + Args: + unit_class: Class of unit to generate. + group: The TheaterGroup to generate the unit/group for. + vehicle_units: List of TheaterUnits. The new unit will be appended to this list. + cp_name: Name of the Control Point. + group_role: Pretense group role, "support" or "assault". + max_num: Maximum number of units to generate per group. + """ + + if self.coalition.faction.has_access_to_unit_class(unit_class): + unit_type = self.ground_unit_of_class(self.coalition, unit_class) + if unit_type is not None and len(vehicle_units) < max_num: + unit_id = self.game.next_unit_id() + unit_name = f"{cp_name}-{group_role}-{unit_id}" + + spread_out_heading = random.randrange(1, 360) + spread_out_position = group.position.point_from_heading( + spread_out_heading, 30 + ) + ground_unit_pos = PointWithHeading.from_point( + spread_out_position, group.position.heading + ) + + theater_unit = TheaterUnit( + unit_id, + unit_name, + unit_type.dcs_unit_type, + ground_unit_pos, + group.ground_object, + ) + vehicle_units.append(theater_unit) + + def generate_amphibious_unit_of_class( + self, + unit_class: UnitClass, + group: TheaterGroup, + vehicle_units: list[TheaterUnit], + cp_name: str, + group_role: str, + max_num: int, + ) -> None: + """ + Generates a single amphibious TheaterUnit for a Pretense unit group + for a specific TheaterGroup, provided that the group still has room + (defined by the max_num argument). Amphibious units are selected + out of a pre-defined list. Units which the faction has access to + are preferred, but certain default unit types are selected as + a fall-back to ensure that all the generated units can swim. + + Generated units are placed 30 meters from the TheaterGroup + position in a random direction. + + Args: + unit_class: Class of unit to generate. + group: The TheaterGroup to generate the unit/group for. + vehicle_units: List of TheaterUnits. The new unit will be appended to this list. + cp_name: Name of the Control Point. + group_role: Pretense group role, "support" or "assault". + max_num: Maximum number of units to generate per group. + """ + unit_type = None + faction = self.coalition.faction + is_player = True + side = ( + 2 + if self.country == self.game.coalition_for(is_player).faction.country + else 1 + ) + default_amphibious_unit = unit_type + default_logistics_unit = unit_type + default_tank_unit_blue = unit_type + default_apc_unit_blue = unit_type + default_ifv_unit_blue = unit_type + default_recon_unit_blue = unit_type + default_atgm_unit_blue = unit_type + default_tank_unit_red = unit_type + default_apc_unit_red = unit_type + default_ifv_unit_red = unit_type + default_recon_unit_red = unit_type + default_atgm_unit_red = unit_type + default_ifv_unit_chinese = unit_type + pretense_amphibious_units = PRETENSE_AMPHIBIOUS_UNITS + random.shuffle(pretense_amphibious_units) + for unit in pretense_amphibious_units: + for groundunittype in GroundUnitType.for_dcs_type(unit): + if unit == vehicles.Unarmed.LARC_V: + default_logistics_unit = groundunittype + elif unit == Char_M551_Sheridan: + default_tank_unit_blue = groundunittype + elif unit == vehicles.Armor.AAV7: + default_apc_unit_blue = groundunittype + elif unit == vehicles.Armor.LAV_25: + default_ifv_unit_blue = groundunittype + elif unit == vehicles.Armor.TPZ: + default_recon_unit_blue = groundunittype + elif unit == vehicles.Armor.VAB_Mephisto: + default_atgm_unit_blue = groundunittype + elif unit == vehicles.Armor.PT_76: + default_tank_unit_red = groundunittype + elif unit == vehicles.Armor.BTR_80: + default_apc_unit_red = groundunittype + elif unit == vehicles.Armor.BMD_1: + default_ifv_unit_red = groundunittype + elif unit == vehicles.Armor.BRDM_2: + default_recon_unit_red = groundunittype + elif unit == vehicles.Armor.BTR_D: + default_atgm_unit_red = groundunittype + elif unit == vehicles.Armor.ZBD04A: + default_ifv_unit_chinese = groundunittype + elif unit == vehicles.Armor.MTLB: + default_amphibious_unit = groundunittype + if self.coalition.faction.has_access_to_dcs_type(unit): + if groundunittype.unit_class == unit_class: + unit_type = groundunittype + break + if unit_type is None: + if unit_class == UnitClass.LOGISTICS: + unit_type = default_logistics_unit + elif faction.country.id == China.id: + unit_type = default_ifv_unit_chinese + elif side == 2 and unit_class == UnitClass.TANK: + if faction.mod_settings is not None and faction.mod_settings.frenchpack: + unit_type = default_tank_unit_blue + else: + unit_type = default_apc_unit_blue + elif side == 2 and unit_class == UnitClass.IFV: + unit_type = default_ifv_unit_blue + elif side == 2 and unit_class == UnitClass.APC: + unit_type = default_apc_unit_blue + elif side == 2 and unit_class == UnitClass.ATGM: + unit_type = default_atgm_unit_blue + elif side == 2 and unit_class == UnitClass.RECON: + unit_type = default_recon_unit_blue + elif side == 1 and unit_class == UnitClass.TANK: + unit_type = default_tank_unit_red + elif side == 1 and unit_class == UnitClass.IFV: + unit_type = default_ifv_unit_red + elif side == 1 and unit_class == UnitClass.APC: + unit_type = default_apc_unit_red + elif side == 1 and unit_class == UnitClass.ATGM: + unit_type = default_atgm_unit_red + elif side == 1 and unit_class == UnitClass.RECON: + unit_type = default_recon_unit_red + else: + unit_type = default_amphibious_unit + if unit_type is not None and len(vehicle_units) < max_num: + unit_id = self.game.next_unit_id() + unit_name = f"{cp_name}-{group_role}-{unit_id}" + + spread_out_heading = random.randrange(1, 360) + spread_out_position = group.position.point_from_heading( + spread_out_heading, 30 + ) + ground_unit_pos = PointWithHeading.from_point( + spread_out_position, group.position.heading + ) + + theater_unit = TheaterUnit( + unit_id, + unit_name, + unit_type.dcs_unit_type, + ground_unit_pos, + group.ground_object, + ) + vehicle_units.append(theater_unit) + + def generate(self) -> None: + if self.culled: + return + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( + self.ground_object.control_point.name + ) + country_name_trimmed = "".join( + [i for i in self.country.shortname.lower() if i.isalpha()] + ) + + for group in self.ground_object.groups: + vehicle_units: list[TheaterUnit] = [] + + for unit in group.units: + if unit.is_static: + # Add supply convoy + group_role = "supply" + group_name = f"{cp_name_trimmed}-{country_name_trimmed}-{group_role}-{group.id}" + group.name = group_name + + self.generate_ground_unit_of_class( + UnitClass.LOGISTICS, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE, + ) + elif unit.is_vehicle and unit.alive: + # Add armor group + group_role = "assault" + group_name = f"{cp_name_trimmed}-{country_name_trimmed}-{group_role}-{group.id}" + group.name = group_name + + self.generate_ground_unit_of_class( + UnitClass.TANK, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE - 4, + ) + self.generate_ground_unit_of_class( + UnitClass.TANK, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE - 3, + ) + self.generate_ground_unit_of_class( + UnitClass.ATGM, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE - 2, + ) + self.generate_ground_unit_of_class( + UnitClass.APC, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE - 1, + ) + self.generate_ground_unit_of_class( + UnitClass.IFV, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE, + ) + self.generate_ground_unit_of_class( + UnitClass.RECON, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE, + ) + if random.randrange(0, 100) > 75: + self.generate_ground_unit_of_class( + UnitClass.SHORAD, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE, + ) + elif unit.is_ship and unit.alive: + # Attach this group to the closest naval group, if available + control_point = self.ground_object.control_point + for ( + other_cp + ) in self.game.theater.closest_friendly_control_points_to( + self.ground_object.control_point + ): + if other_cp.is_fleet: + control_point = other_cp + break + + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( + control_point.name + ) + is_player = True + side = ( + 2 + if self.country + == self.game.coalition_for(is_player).faction.country + else 1 + ) + + try: + number_of_supply_groups = len( + self.game.pretense_ground_supply[side][cp_name_trimmed] + ) + except KeyError: + number_of_supply_groups = 0 + self.game.pretense_ground_supply[side][cp_name_trimmed] = list() + self.game.pretense_ground_assault[side][ + cp_name_trimmed + ] = list() + + if number_of_supply_groups == 0: + # Add supply convoy + group_role = "supply" + group_name = f"{cp_name_trimmed}-{country_name_trimmed}-{group_role}-{group.id}" + group.name = group_name + + self.generate_amphibious_unit_of_class( + UnitClass.LOGISTICS, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE, + ) + else: + # Add armor group + group_role = "assault" + group_name = f"{cp_name_trimmed}-{country_name_trimmed}-{group_role}-{group.id}" + group.name = group_name + + self.generate_amphibious_unit_of_class( + UnitClass.TANK, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE - 4, + ) + self.generate_amphibious_unit_of_class( + UnitClass.TANK, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE - 3, + ) + self.generate_amphibious_unit_of_class( + UnitClass.ATGM, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE - 2, + ) + self.generate_amphibious_unit_of_class( + UnitClass.APC, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE - 1, + ) + self.generate_amphibious_unit_of_class( + UnitClass.IFV, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE, + ) + self.generate_amphibious_unit_of_class( + UnitClass.RECON, + group, + vehicle_units, + cp_name_trimmed, + group_role, + PRETENSE_GROUND_UNIT_GROUP_SIZE, + ) + if vehicle_units: + self.create_vehicle_group(group.group_name, vehicle_units) + + def create_vehicle_group( + self, group_name: str, units: list[TheaterUnit] + ) -> VehicleGroup: + vehicle_group: Optional[VehicleGroup] = None + + control_point = self.ground_object.control_point + for unit in self.ground_object.units: + if unit.is_ship: + # Unit is naval/amphibious. Attach this group to the closest naval group, if available. + for other_cp in self.game.theater.closest_friendly_control_points_to( + self.ground_object.control_point + ): + if other_cp.is_fleet: + control_point = other_cp + break + + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( + control_point.name + ) + is_player = True + side = ( + 2 + if self.country == self.game.coalition_for(is_player).faction.country + else 1 + ) + + for unit in units: + assert issubclass(unit.type, VehicleType) + faction = self.coalition.faction + if vehicle_group is None: + vehicle_group = self.m.vehicle_group( + self.country, + group_name, + unit.type, + position=unit.position, + heading=unit.position.heading.degrees, + ) + vehicle_group.units[0].player_can_drive = True + self.enable_eplrs(vehicle_group, unit.type) + vehicle_group.units[0].name = unit.unit_name + self.set_alarm_state(vehicle_group) + GroundForcePainter(faction, vehicle_group.units[0]).apply_livery() + + group_role = group_name.split("-")[2] + if group_role == "supply": + self.game.pretense_ground_supply[side][cp_name_trimmed].append( + f"{vehicle_group.name}" + ) + elif group_role == "assault": + self.game.pretense_ground_assault[side][cp_name_trimmed].append( + f"{vehicle_group.name}" + ) + else: + vehicle_unit = self.m.vehicle(unit.unit_name, unit.type) + vehicle_unit.player_can_drive = True + vehicle_unit.position = unit.position + vehicle_unit.heading = unit.position.heading.degrees + GroundForcePainter(faction, vehicle_unit).apply_livery() + vehicle_group.add_unit(vehicle_unit) + self._register_theater_unit(unit, vehicle_group.units[-1]) + if vehicle_group is None: + raise RuntimeError(f"Error creating VehicleGroup for {group_name}") + return vehicle_group + + +class PretenseGenericCarrierGenerator(GenericCarrierGenerator): + """Base type for carrier group generation. + + Used by both CV(N) groups and LHA groups. + """ + + def __init__( + self, + ground_object: GenericCarrierGroundObject, + control_point: NavalControlPoint, + country: Country, + game: Game, + mission: Mission, + radio_registry: RadioRegistry, + tacan_registry: TacanRegistry, + icls_alloc: Iterator[int], + runways: Dict[str, RunwayData], + unit_map: UnitMap, + mission_data: MissionData, + ) -> None: + super().__init__( + ground_object, + control_point, + country, + game, + mission, + radio_registry, + tacan_registry, + icls_alloc, + runways, + unit_map, + mission_data, + ) + self.ground_object = ground_object + self.control_point = control_point + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry + self.icls_alloc = icls_alloc + self.runways = runways + self.mission_data = mission_data + + def generate(self) -> None: + if self.control_point.frequency is not None: + atc = self.control_point.frequency + if atc not in self.radio_registry.allocated_channels: + self.radio_registry.reserve(atc) + else: + atc = self.radio_registry.alloc_uhf() + + for g_id, group in enumerate(self.ground_object.groups): + if not group.units: + logging.warning(f"Found empty carrier group in {self.control_point}") + continue + + ship_units = [] + for unit in group.units: + if unit.alive: + # All alive Ships + print( + f"Added {unit.unit_name} to ship_units of group {group.group_name}" + ) + ship_units.append(unit) + + if not ship_units: + # Empty array (no alive units), skip this group + continue + + ship_group = self.create_ship_group(group.group_name, ship_units, atc) + + if self.game.settings.pretense_carrier_steams_into_wind: + # Always steam into the wind, even if the carrier is being moved. + # There are multiple unsimulated hours between turns, so we can + # count those as the time the carrier uses to move and the mission + # time as the recovery window. + brc = self.steam_into_wind(ship_group) + else: + brc = Heading(0) + + # Set Carrier Specific Options + if g_id == 0 and self.control_point.runway_is_operational(): + # Get Correct unit type for the carrier. + # This will upgrade to super carrier if option is enabled + carrier_type = self.carrier_type + if carrier_type is None: + raise RuntimeError( + f"Error generating carrier group for {self.control_point.name}" + ) + ship_group.units[0].type = carrier_type.id + if self.control_point.tacan is None: + tacan = self.tacan_registry.alloc_for_band( + TacanBand.X, TacanUsage.TransmitReceive + ) + else: + tacan = self.control_point.tacan + if self.control_point.tcn_name is None: + tacan_callsign = self.tacan_callsign() + else: + tacan_callsign = self.control_point.tcn_name + link4 = None + link4carriers = [Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal] + if carrier_type in link4carriers: + if self.control_point.link4 is None: + link4 = self.radio_registry.alloc_uhf() + else: + link4 = self.control_point.link4 + icls = None + icls_name = self.control_point.icls_name + if carrier_type in link4carriers or carrier_type == LHA_Tarawa: + if self.control_point.icls_channel is None: + icls = next(self.icls_alloc) + else: + icls = self.control_point.icls_channel + self.activate_beacons( + ship_group, tacan, tacan_callsign, icls, icls_name, link4 + ) + self.add_runway_data( + brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls + ) + self.mission_data.carriers.append( + CarrierInfo( + group_name=ship_group.name, + unit_name=ship_group.units[0].name, + callsign=tacan_callsign, + freq=atc, + tacan=tacan, + icls_channel=icls, + link4_freq=link4, + blue=self.control_point.captured, + ) + ) + + +class PretenseCarrierGenerator(PretenseGenericCarrierGenerator): + def tacan_callsign(self) -> str: + # TODO: Assign these properly. + return random.choice( + [ + "STE", + "CVN", + "CVH", + "CCV", + "ACC", + "ARC", + "GER", + "ABR", + "LIN", + "TRU", + ] + ) + + +class PretenseLhaGenerator(PretenseGenericCarrierGenerator): + def tacan_callsign(self) -> str: + # TODO: Assign these properly. + return random.choice( + [ + "LHD", + "LHA", + "LHB", + "LHC", + "LHD", + "LDS", + ] + ) + + +class PretenseTgoGenerator(TgoGenerator): + """Creates DCS groups and statics for the theater during mission generation. + + Most of the work of group/static generation is delegated to the other + generator classes. This class is responsible for finding each of the + locations for spawning ground objects, determining their types, and creating + the appropriate generators. + """ + + def __init__( + self, + mission: Mission, + game: Game, + radio_registry: RadioRegistry, + tacan_registry: TacanRegistry, + unit_map: UnitMap, + mission_data: MissionData, + ) -> None: + super().__init__( + mission, + game, + radio_registry, + tacan_registry, + unit_map, + mission_data, + ) + + self.m = mission + self.game = game + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry + self.unit_map = unit_map + self.icls_alloc = iter(range(1, 21)) + self.runways: Dict[str, RunwayData] = {} + self.helipads: dict[ControlPoint, list[StaticGroup]] = defaultdict(list) + self.ground_spawns_roadbase: dict[ + ControlPoint, list[Tuple[StaticGroup, Point]] + ] = defaultdict(list) + self.ground_spawns: dict[ + ControlPoint, list[Tuple[StaticGroup, Point]] + ] = defaultdict(list) + self.mission_data = mission_data + + def generate(self) -> None: + for cp in self.game.theater.controlpoints: + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp.name) + for side in range(1, 3): + if cp_name_trimmed not in self.game.pretense_ground_supply[side]: + self.game.pretense_ground_supply[side][cp_name_trimmed] = list() + if cp_name_trimmed not in self.game.pretense_ground_assault[side]: + self.game.pretense_ground_assault[side][cp_name_trimmed] = list() + + # First generate units for the coalition, which initially holds this CP + country = self.m.country(cp.coalition.faction.country.name) + + # Generate helipads + helipad_gen = HelipadGenerator( + self.m, cp, self.game, self.radio_registry, self.tacan_registry + ) + helipad_gen.generate() + self.helipads[cp] = helipad_gen.helipads + + # Generate Highway Strip slots + ground_spawn_roadbase_gen = GroundSpawnRoadbaseGenerator( + self.m, cp, self.game, self.radio_registry, self.tacan_registry + ) + ground_spawn_roadbase_gen.generate() + self.ground_spawns_roadbase[ + cp + ] = ground_spawn_roadbase_gen.ground_spawns_roadbase + random.shuffle(self.ground_spawns_roadbase[cp]) + + # Generate STOL pads + ground_spawn_gen = GroundSpawnGenerator( + self.m, cp, self.game, self.radio_registry, self.tacan_registry + ) + ground_spawn_gen.generate() + self.ground_spawns[cp] = ground_spawn_gen.ground_spawns + random.shuffle(self.ground_spawns[cp]) + + for ground_object in cp.ground_objects: + generator: GroundObjectGenerator + if isinstance(ground_object, CarrierGroundObject) and isinstance( + cp, NavalControlPoint + ): + generator = PretenseCarrierGenerator( + ground_object, + cp, + country, + self.game, + self.m, + self.radio_registry, + self.tacan_registry, + self.icls_alloc, + self.runways, + self.unit_map, + self.mission_data, + ) + elif isinstance(ground_object, LhaGroundObject) and isinstance( + cp, NavalControlPoint + ): + generator = PretenseLhaGenerator( + ground_object, + cp, + country, + self.game, + self.m, + self.radio_registry, + self.tacan_registry, + self.icls_alloc, + self.runways, + self.unit_map, + self.mission_data, + ) + elif isinstance(ground_object, MissileSiteGroundObject): + generator = MissileSiteGenerator( + ground_object, country, self.game, self.m, self.unit_map + ) + else: + generator = PretenseGroundObjectGenerator( + ground_object, country, self.game, self.m, self.unit_map + ) + generator.generate() + # Then generate ground supply and assault groups for the other coalition + other_coalition = cp.coalition + for coalition in cp.coalition.game.coalitions: + if coalition == cp.coalition: + continue + else: + other_coalition = coalition + country = self.m.country(other_coalition.faction.country.name) + new_ground_object: TheaterGroundObject + for ground_object in cp.ground_objects: + if isinstance(ground_object, BuildingGroundObject): + new_ground_object = BuildingGroundObject( + name=ground_object.name, + category=ground_object.category, + location=PresetLocation( + f"{ground_object.name} {ground_object.id}", + ground_object.position, + ground_object.heading, + ), + control_point=ground_object.control_point, + is_fob_structure=ground_object.is_fob_structure, + task=ground_object.task, + ) + new_ground_object.groups = ground_object.groups + generator = PretenseGroundObjectGenerator( + new_ground_object, country, self.game, self.m, self.unit_map + ) + elif isinstance(ground_object, VehicleGroupGroundObject): + new_ground_object = VehicleGroupGroundObject( + name=ground_object.name, + location=PresetLocation( + f"{ground_object.name} {ground_object.id}", + ground_object.position, + ground_object.heading, + ), + control_point=ground_object.control_point, + task=ground_object.task, + ) + new_ground_object.groups = ground_object.groups + generator = PretenseGroundObjectGenerator( + new_ground_object, country, self.game, self.m, self.unit_map + ) + else: + continue + + generator.coalition = other_coalition + generator.generate() + + self.mission_data.runways = list(self.runways.values()) diff --git a/game/pretense/pretensetriggergenerator.py b/game/pretense/pretensetriggergenerator.py new file mode 100644 index 00000000..1f1bd829 --- /dev/null +++ b/game/pretense/pretensetriggergenerator.py @@ -0,0 +1,475 @@ +from __future__ import annotations + +import logging +import math +import random +from typing import TYPE_CHECKING, List + +from dcs import Point +from dcs.action import ( + ClearFlag, + DoScript, + MarkToAll, + SetFlag, + RemoveSceneObjects, + RemoveSceneObjectsMask, + SceneryDestructionZone, + Smoke, +) +from dcs.condition import ( + AllOfCoalitionOutsideZone, + FlagIsFalse, + FlagIsTrue, + PartOfCoalitionInZone, + TimeAfter, + TimeSinceFlag, +) +from dcs.mission import Mission +from dcs.task import Option +from dcs.terrain.caucasus.airports import Krasnodar_Pashkovsky +from dcs.terrain.syria.airports import Damascus, Khalkhalah +from dcs.translation import String +from dcs.triggers import Event, TriggerCondition, TriggerOnce +from dcs.unit import Skill +from numpy import cross, einsum, arctan2 +from shapely import MultiPolygon, Point as ShapelyPoint + +from game.naming import ALPHA_MILITARY +from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator +from game.theater import Airfield +from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE, OffMapSpawn + +if TYPE_CHECKING: + from game.game import Game + +PUSH_TRIGGER_SIZE = 3000 +PUSH_TRIGGER_ACTIVATION_AGL = 25 + +REGROUP_ZONE_DISTANCE = 12000 +REGROUP_ALT = 5000 + +TRIGGER_WAYPOINT_OFFSET = 2 +TRIGGER_MIN_DISTANCE_FROM_START = 10000 +# modified since we now have advanced SAM units +TRIGGER_RADIUS_MINIMUM = 3000000 + +TRIGGER_RADIUS_SMALL = 50000 +TRIGGER_RADIUS_MEDIUM = 100000 +TRIGGER_RADIUS_LARGE = 150000 +TRIGGER_RADIUS_ALL_MAP = 3000000 +TRIGGER_RADIUS_CLEAR_SCENERY = 1000 +TRIGGER_RADIUS_PRETENSE_TGO = 500 +TRIGGER_RADIUS_PRETENSE_SUPPLY = 500 +TRIGGER_RADIUS_PRETENSE_HELI = 1000 +TRIGGER_RADIUS_PRETENSE_HELI_BUFFER = 500 +TRIGGER_RADIUS_PRETENSE_CARRIER = 20000 +TRIGGER_RADIUS_PRETENSE_CARRIER_SMALL = 3000 +TRIGGER_RADIUS_PRETENSE_CARRIER_CORNER = 25000 +TRIGGER_RUNWAY_LENGTH_PRETENSE = 2500 +TRIGGER_RUNWAY_WIDTH_PRETENSE = 400 + +SIMPLIFY_RUNS_PRETENSE_CARRIER = 10000 + + +class Silence(Option): + Key = 7 + + +class PretenseTriggerGenerator: + capture_zone_types = (Fob, Airfield) + capture_zone_flag = 600 + + def __init__(self, mission: Mission, game: Game) -> None: + self.mission = mission + self.game = game + + def _set_allegiances(self, player_coalition: str, enemy_coalition: str) -> None: + """ + Set airbase initial coalition + """ + + # Empty neutrals airports + airfields = [ + cp for cp in self.game.theater.controlpoints if isinstance(cp, Airfield) + ] + airport_ids = {cp.airport.id for cp in airfields} + for airport in self.mission.terrain.airport_list(): + if airport.id not in airport_ids: + airport.unlimited_fuel = False + airport.unlimited_munitions = False + airport.unlimited_aircrafts = False + airport.gasoline_init = 0 + airport.methanol_mixture_init = 0 + airport.diesel_init = 0 + airport.jet_init = 0 + airport.operating_level_air = 0 + airport.operating_level_equipment = 0 + airport.operating_level_fuel = 0 + + for airport in self.mission.terrain.airport_list(): + if airport.id not in airport_ids: + airport.unlimited_fuel = True + airport.unlimited_munitions = True + airport.unlimited_aircrafts = True + + for airfield in airfields: + cp_airport = self.mission.terrain.airport_by_id(airfield.airport.id) + if cp_airport is None: + raise RuntimeError( + f"Could not find {airfield.airport.name} in the mission" + ) + cp_airport.set_coalition( + airfield.captured and player_coalition or enemy_coalition + ) + + def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None: + """ + Set skill level for all aircraft in the mission + """ + for coalition_name, coalition in self.mission.coalition.items(): + if coalition_name == player_coalition: + skill_level = Skill(self.game.settings.player_skill) + elif coalition_name == enemy_coalition: + skill_level = Skill(self.game.settings.enemy_vehicle_skill) + else: + continue + + for country in coalition.countries.values(): + for vehicle_group in country.vehicle_group: + vehicle_group.set_skill(skill_level) + + def _gen_markers(self) -> None: + """ + Generate markers on F10 map for each existing objective + """ + if self.game.settings.generate_marks: + mark_trigger = TriggerOnce(Event.NoEvent, "Marks generator") + mark_trigger.add_condition(TimeAfter(1)) + v = 10 + for cp in self.game.theater.controlpoints: + seen = set() + for ground_object in cp.ground_objects: + if ground_object.obj_name in seen: + continue + + seen.add(ground_object.obj_name) + for location in ground_object.mark_locations: + zone = self.mission.triggers.add_triggerzone( + location, radius=10, hidden=True, name="MARK" + ) + if cp.captured: + name = ground_object.obj_name + " [ALLY]" + else: + name = ground_object.obj_name + " [ENEMY]" + mark_trigger.add_action(MarkToAll(v, zone.id, String(name))) + v += 1 + self.mission.triggerrules.triggers.append(mark_trigger) + + def _generate_capture_triggers( + self, player_coalition: str, enemy_coalition: str + ) -> None: + """Creates a pair of triggers for each control point of `cls.capture_zone_types`. + One for the initial capture of a control point, and one if it is recaptured. + Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua` + """ + for cp in self.game.theater.controlpoints: + if isinstance(cp, self.capture_zone_types) and not cp.is_carrier: + if cp.captured: + attacking_coalition = enemy_coalition + attack_coalition_int = 1 # 1 is the Event int for Red + defending_coalition = player_coalition + defend_coalition_int = 2 # 2 is the Event int for Blue + else: + attacking_coalition = player_coalition + attack_coalition_int = 2 + defending_coalition = enemy_coalition + defend_coalition_int = 1 + + trigger_zone = self.mission.triggers.add_triggerzone( + cp.position, + radius=TRIGGER_RADIUS_CAPTURE, + hidden=False, + name="CAPTURE", + ) + flag = self.get_capture_zone_flag() + capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger") + capture_trigger.add_condition( + AllOfCoalitionOutsideZone( + defending_coalition, trigger_zone.id, unit_type="GROUND" + ) + ) + capture_trigger.add_condition( + PartOfCoalitionInZone( + attacking_coalition, trigger_zone.id, unit_type="GROUND" + ) + ) + capture_trigger.add_condition(FlagIsFalse(flag=flag)) + script_string = String( + f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{attack_coalition_int}||{cp.full_name}"' + ) + capture_trigger.add_action(DoScript(script_string)) + capture_trigger.add_action(SetFlag(flag=flag)) + self.mission.triggerrules.triggers.append(capture_trigger) + + recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger") + recapture_trigger.add_condition( + AllOfCoalitionOutsideZone( + attacking_coalition, trigger_zone.id, unit_type="GROUND" + ) + ) + recapture_trigger.add_condition( + PartOfCoalitionInZone( + defending_coalition, trigger_zone.id, unit_type="GROUND" + ) + ) + recapture_trigger.add_condition(FlagIsTrue(flag=flag)) + script_string = String( + f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{defend_coalition_int}||{cp.full_name}"' + ) + recapture_trigger.add_action(DoScript(script_string)) + recapture_trigger.add_action(ClearFlag(flag=flag)) + self.mission.triggerrules.triggers.append(recapture_trigger) + + def _generate_pretense_zone_triggers(self) -> None: + """Creates triggger zones for the Pretense campaign. These include: + - Carrier zones for friendly forces, generated from the navmesh / sea zone intersection + - Carrier zones for opposing forces + - Airfield and FARP zones + - Airfield and FARP spawn points / helicopter spawn points / ground object positions + """ + + # First generate carrier zones for friendly forces + use_blue_navmesh = ( + self.game.settings.pretense_carrier_zones_navmesh == "Blue navmesh" + ) + sea_zones_landmap = self.game.coalition_for( + player=False + ).nav_mesh.theater.landmap + if ( + self.game.settings.pretense_controllable_carrier + and sea_zones_landmap is not None + ): + navmesh_number = 0 + for navmesh_poly in self.game.coalition_for( + player=use_blue_navmesh + ).nav_mesh.polys: + navmesh_number += 1 + if sea_zones_landmap.sea_zones.intersects(navmesh_poly.poly): + # Get the intersection between the navmesh zone and the sea zone + navmesh_sea_intersection = sea_zones_landmap.sea_zones.intersection( + navmesh_poly.poly + ) + navmesh_zone_verticies = navmesh_sea_intersection + + # Simplify it to get a quadrangle + for simplify_run in range(SIMPLIFY_RUNS_PRETENSE_CARRIER): + navmesh_zone_verticies = navmesh_sea_intersection.simplify( + float(simplify_run * 10), preserve_topology=False + ) + if isinstance(navmesh_zone_verticies, MultiPolygon): + break + if len(navmesh_zone_verticies.exterior.coords) <= 4: + break + if isinstance(navmesh_zone_verticies, MultiPolygon): + continue + trigger_zone_verticies = [] + terrain = self.game.theater.terrain + alpha = random.choice(ALPHA_MILITARY) + + # Generate the quadrangle zone and four points inside it for carrier navigation + if len(navmesh_zone_verticies.exterior.coords) == 4: + zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15} + corner_point_num = 0 + for point_coord in navmesh_zone_verticies.exterior.coords: + corner_point = Point( + x=point_coord[0], y=point_coord[1], terrain=terrain + ) + nav_point = corner_point.point_from_heading( + corner_point.heading_between_point( + navmesh_sea_intersection.centroid + ), + TRIGGER_RADIUS_PRETENSE_CARRIER_CORNER, + ) + corner_point_num += 1 + + zone_name = f"{alpha}-{navmesh_number}-{corner_point_num}" + if sea_zones_landmap.sea_zones.contains( + ShapelyPoint(nav_point.x, nav_point.y) + ): + self.mission.triggers.add_triggerzone( + nav_point, + radius=TRIGGER_RADIUS_PRETENSE_CARRIER_SMALL, + hidden=False, + name=zone_name, + color=zone_color, + ) + + trigger_zone_verticies.append(corner_point) + + zone_name = f"{alpha}-{navmesh_number}" + trigger_zone = self.mission.triggers.add_triggerzone_quad( + navmesh_sea_intersection.centroid, + trigger_zone_verticies, + hidden=False, + name=zone_name, + color=zone_color, + ) + try: + if len(self.game.pretense_carrier_zones) == 0: + self.game.pretense_carrier_zones = [] + except AttributeError: + self.game.pretense_carrier_zones = [] + self.game.pretense_carrier_zones.append(zone_name) + + for cp in self.game.theater.controlpoints: + if ( + cp.is_fleet + and self.game.settings.pretense_controllable_carrier + and cp.captured + ): + # Friendly carrier zones are generated above + continue + elif cp.is_fleet: + trigger_radius = float(TRIGGER_RADIUS_PRETENSE_CARRIER) + elif isinstance(cp, Fob) and cp.has_helipads: + trigger_radius = TRIGGER_RADIUS_PRETENSE_HELI + for helipad in list( + cp.helipads + cp.helipads_quad + cp.helipads_invisible + ): + if cp.position.distance_to_point(helipad) > trigger_radius: + trigger_radius = cp.position.distance_to_point(helipad) + for ground_spawn, ground_spawn_wp in list( + cp.ground_spawns + cp.ground_spawns_roadbase + ): + if cp.position.distance_to_point(ground_spawn) > trigger_radius: + trigger_radius = cp.position.distance_to_point(ground_spawn) + trigger_radius += TRIGGER_RADIUS_PRETENSE_HELI_BUFFER + else: + if cp.dcs_airport is not None and ( + isinstance(cp.dcs_airport, Damascus) + or isinstance(cp.dcs_airport, Khalkhalah) + or isinstance(cp.dcs_airport, Krasnodar_Pashkovsky) + ): + # Increase the size of Pretense zones at Damascus, Khalkhalah and Krasnodar-Pashkovsky + # (which are quite spread out) so the zone would encompass the entire airfield. + trigger_radius = int(TRIGGER_RADIUS_CAPTURE * 1.8) + else: + trigger_radius = TRIGGER_RADIUS_CAPTURE + cp_name = "".join( + [i for i in cp.name if i.isalnum() or i.isspace() or i == "-"] + ) + cp_name = cp_name.replace("Ä", "A") + cp_name = cp_name.replace("Ö", "O") + cp_name = cp_name.replace("Ø", "O") + cp_name = cp_name.replace("ä", "a") + cp_name = cp_name.replace("ö", "o") + cp_name = cp_name.replace("ø", "o") + if not isinstance(cp, OffMapSpawn): + zone_color = {1: 0.0, 2: 0.0, 3: 0.0, 4: 0.15} + self.mission.triggers.add_triggerzone( + cp.position, + radius=trigger_radius, + hidden=False, + name=cp_name, + color=zone_color, + ) + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(cp.name) + tgo_num = 0 + for tgo in cp.ground_objects: + if cp.is_fleet or tgo.sea_object: + continue + tgo_num += 1 + zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15} + self.mission.triggers.add_triggerzone( + tgo.position, + radius=TRIGGER_RADIUS_PRETENSE_TGO, + hidden=False, + name=f"{cp_name_trimmed}-{tgo_num}", + color=zone_color, + ) + for helipad in cp.helipads + cp.helipads_invisible + cp.helipads_quad: + zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15} + self.mission.triggers.add_triggerzone( + position=helipad, + radius=TRIGGER_RADIUS_PRETENSE_HELI, + hidden=False, + name=f"{cp_name_trimmed}-hsp", + color=zone_color, + ) + break + for supply_route in cp.convoy_routes.values(): + tgo_num += 1 + zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15} + origin_position = supply_route[0] + next_position = supply_route[1] + convoy_heading = origin_position.heading_between_point(next_position) + supply_position = origin_position.point_from_heading( + convoy_heading, 300 + ) + self.mission.triggers.add_triggerzone( + supply_position, + radius=TRIGGER_RADIUS_PRETENSE_TGO, + hidden=False, + name=f"{cp_name_trimmed}-sp", + color=zone_color, + ) + break + airfields = [ + cp for cp in self.game.theater.controlpoints if isinstance(cp, Airfield) + ] + for airfield in airfields: + cp_airport = self.mission.terrain.airport_by_id(airfield.airport.id) + if cp_airport is None: + continue + cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( + cp_airport.name + ) + zone_color = {1: 0.0, 2: 1.0, 3: 0.5, 4: 0.15} + if cp_airport is None: + raise RuntimeError( + f"Could not find {airfield.airport.name} in the mission" + ) + for runway in cp_airport.runways: + runway_end_1 = cp_airport.position.point_from_heading( + runway.heading, TRIGGER_RUNWAY_LENGTH_PRETENSE / 2 + ) + runway_end_2 = cp_airport.position.point_from_heading( + runway.heading + 180, TRIGGER_RUNWAY_LENGTH_PRETENSE / 2 + ) + runway_verticies = [ + runway_end_1.point_from_heading( + runway.heading - 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2 + ), + runway_end_1.point_from_heading( + runway.heading + 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2 + ), + runway_end_2.point_from_heading( + runway.heading + 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2 + ), + runway_end_2.point_from_heading( + runway.heading - 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2 + ), + ] + trigger_zone = self.mission.triggers.add_triggerzone_quad( + cp_airport.position, + runway_verticies, + hidden=False, + name=f"{cp_name_trimmed}-runway-{runway.id}", + color=zone_color, + ) + break + + def generate(self) -> None: + player_coalition = "blue" + enemy_coalition = "red" + + self._set_skill(player_coalition, enemy_coalition) + self._set_allegiances(player_coalition, enemy_coalition) + self._generate_pretense_zone_triggers() + self._generate_capture_triggers(player_coalition, enemy_coalition) + + @classmethod + def get_capture_zone_flag(cls) -> int: + flag = cls.capture_zone_flag + cls.capture_zone_flag += 1 + return flag diff --git a/game/radio/radios.py b/game/radio/radios.py index 83d695d9..2cab104f 100644 --- a/game/radio/radios.py +++ b/game/radio/radios.py @@ -416,15 +416,19 @@ class RadioRegistry: already allocated. """ try: + while_count = 0 while (channel := random_frequency(radio)) in self.allocated_channels: + while_count += 1 + if while_count > 1000: + raise StopIteration pass self.reserve(channel) return channel except StopIteration: # In the event of too many channel users, fail gracefully by reusing - # the last channel. + # a channel. # https://github.com/dcs-liberation/dcs_liberation/issues/598 - channel = radio.last_channel + channel = random_frequency(radio) logging.warning( f"No more free channels for {radio.name}. Reusing {channel}." ) diff --git a/game/settings/settings.py b/game/settings/settings.py index ca7b8119..301bddf0 100644 --- a/game/settings/settings.py +++ b/game/settings/settings.py @@ -49,6 +49,8 @@ FLIGHT_PLANNER_AUTOMATION = "Flight Planner Automation" CAMPAIGN_DOCTRINE_PAGE = "Campaign Doctrine" DOCTRINE_DISTANCES_SECTION = "Doctrine distances" +PRETENSE_PAGE = "Pretense" + MISSION_GENERATOR_PAGE = "Mission Generator" GAMEPLAY_SECTION = "Gameplay" @@ -156,6 +158,7 @@ class Settings: MISSION_RESTRICTIONS_SECTION, default=True, ) + easy_communication: Optional[bool] = choices_option( "Easy Communication", page=DIFFICULTY_PAGE, @@ -174,6 +177,20 @@ class Settings: # Campaign management # General + squadron_random_chance: int = bounded_int_option( + "Percentage of randomly selected aircraft types (only for generated squadrons)", + page=CAMPAIGN_MANAGEMENT_PAGE, + section=GENERAL_SECTION, + default=50, + min=0, + max=100, + detail=( + "

Aircraft type selection is governed by the campaign and the squadron definitions available to " + "Retribution. Squadrons are generated by Retribution if the faction does not have access to the campaign " + "designer's squadron/aircraft definitions. Use the above to increase/decrease aircraft variety by making " + "some selections random instead of picking aircraft types from a priority list.

" + ), + ) restrict_weapons_by_date: bool = boolean_option( "Restrict weapons by date (WIP)", page=CAMPAIGN_MANAGEMENT_PAGE, @@ -906,6 +923,17 @@ class Settings: "Needed to cold-start some aircraft types. Might have a performance impact." ), ) + ground_start_airbase_statics_farps_remove: bool = boolean_option( + "Remove ground spawn statics, including invisible FARPs, at airbases", + MISSION_GENERATOR_PAGE, + GAMEPLAY_SECTION, + default=True, + detail=( + "Ammo and fuel statics and invisible FARPs should be unnecessary when creating " + "additional spawns for players at airbases. This setting will disable them and " + "potentially grant a marginal performance benefit." + ), + ) ai_unlimited_fuel: bool = boolean_option( "AI flights have unlimited fuel", MISSION_GENERATOR_PAGE, @@ -1085,6 +1113,140 @@ class Settings: "if the start-up type was manually changed to 'In-Flight'." ), ) + pretense_maxdistfromfront_distance: int = bounded_int_option( + "Max distance from front (km)", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=130, + min=10, + max=10000, + detail=( + "Zones farther away than this from the front line are switched " + "into low activity state, but will still be there as functional " + "parts of the economy. Use this to adjust performance." + ), + ) + pretense_controllable_carrier: bool = boolean_option( + "Controllable carrier", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=True, + detail=( + "This can be used to enable or disable the native carrier support in Pretense. The Pretense carrier " + "can be controlled through the communication menu (if the Pretense character has enough rank/CMD points) " + "and the player can call in AI aerial and cruise missile missions using it." + "The controllable carriers in Pretense do not build and deploy AI missions autonomously, so if you prefer " + "to have both sides deploy carrier aviation autonomously, you might want to disable this option. " + "When this option is disabled, moving the carrier can only be done with the Retribution interface." + ), + ) + pretense_carrier_steams_into_wind: bool = boolean_option( + "Carriers steam into wind", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=True, + detail=( + "This setting controls whether carriers and their escorts will steam into wind. Disable to " + "to ensure that the carriers stay within the carrier zone in Pretense, but note that " + "doing so might limit carrier operations, takeoff weights and landings." + ), + ) + pretense_carrier_zones_navmesh: str = choices_option( + "Navmesh to use for Pretense carrier zones", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + choices=["Blue navmesh", "Red navmesh"], + default="Blue navmesh", + detail=( + "Use the Retribution map interface options to compare the blue navmesh and the red navmesh." + "You can select which navmesh to use when generating the zones in which the controllable carrier(s) " + "move and operate." + ), + ) + pretense_extra_zone_connections: int = bounded_int_option( + "Extra friendly zone connections", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=2, + min=0, + max=10, + detail=( + "Add connections from each zone to this many closest friendly zones," + "which don't have an existing supply route defined in the campaign." + ), + ) + pretense_num_of_cargo_planes: int = bounded_int_option( + "Number of cargo planes per side", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=2, + min=1, + max=100, + ) + pretense_sead_flights_per_cp: int = bounded_int_option( + "Number of AI SEAD flights per control point / zone", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=1, + min=1, + max=10, + ) + pretense_cas_flights_per_cp: int = bounded_int_option( + "Number of AI CAS flights per control point / zone", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=1, + min=1, + max=10, + ) + pretense_bai_flights_per_cp: int = bounded_int_option( + "Number of AI BAI flights per control point / zone", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=1, + min=1, + max=10, + ) + pretense_strike_flights_per_cp: int = bounded_int_option( + "Number of AI Strike flights per control point / zone", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=1, + min=1, + max=10, + ) + pretense_barcap_flights_per_cp: int = bounded_int_option( + "Number of AI BARCAP flights per control point / zone", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=1, + min=1, + max=10, + ) + pretense_ai_aircraft_per_flight: int = bounded_int_option( + "Number of AI aircraft per flight", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=2, + min=1, + max=4, + ) + pretense_player_flights_per_type: int = bounded_int_option( + "Number of player flights per aircraft type at each base", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=1, + min=1, + max=10, + ) + pretense_ai_cargo_planes_per_side: int = bounded_int_option( + "Number of AI cargo planes per side", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=2, + min=1, + max=20, + ) # Cheating. Not using auto settings because the same page also has buttons which do # not alter settings. diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 1c67d1e0..875a2659 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -202,6 +202,29 @@ class ConflictTheater: assert closest_red is not None return closest_blue, closest_red + def closest_friendly_control_points_to( + self, cp: ControlPoint + ) -> List[ControlPoint]: + """ + Returns a list of the friendly ControlPoints in theater to ControlPoint cp, sorted closest to farthest. + """ + closest_cps = list() + distances_to_cp = dict() + if cp.captured: + control_points = self.player_points() + else: + control_points = self.enemy_points() + for other_cp in control_points: + if cp == other_cp: + continue + + dist = other_cp.position.distance_to_point(cp.position) + distances_to_cp[dist] = other_cp + for i in sorted(distances_to_cp.keys()): + closest_cps.append(distances_to_cp[i]) + + return closest_cps + def find_control_point_by_id(self, cp_id: UUID) -> ControlPoint: for i in self.controlpoints: if i.id == cp_id: diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index f2300410..63bfe1bb 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -32,6 +32,9 @@ def load_icons(): "./resources/ui/misc/" + get_theme_icons() + "/github.png" ) ICONS["Ukraine"] = QPixmap("./resources/ui/misc/ukraine.png") + ICONS["Pretense"] = QPixmap("./resources/ui/misc/pretense.png") + ICONS["Pretense_discord"] = QPixmap("./resources/ui/misc/pretense_discord.png") + ICONS["Pretense_generate"] = QPixmap("./resources/ui/misc/pretense_generate.png") ICONS["Control Points"] = QPixmap( "./resources/ui/misc/" + get_theme_icons() + "/circle.png" diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 59e4b109..dc8ac0f2 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -1,6 +1,7 @@ import logging import traceback import webbrowser +from datetime import datetime from pathlib import Path from typing import Optional @@ -21,6 +22,8 @@ from game import Game, VERSION, persistency, Migrator from game.debriefing import Debriefing from game.game import TurnState from game.layout import LAYOUTS +from game.persistency import pre_pretense_backups_dir +from game.pretense.pretensemissiongenerator import PretenseMissionGenerator from game.server import EventStream, GameContext from game.server.dependencies import QtCallbacks, QtContext from game.theater import ControlPoint, MissionTarget, TheaterGroundObject @@ -193,6 +196,20 @@ class QLiberationWindow(QMainWindow): lambda: webbrowser.open_new_tab("https://shdwp.github.io/ukraine/") ) + self.pretenseLinkAction = QAction("&DCS: Pretense", self) + self.pretenseLinkAction.setIcon(QIcon(CONST.ICONS["Pretense_discord"])) + self.pretenseLinkAction.triggered.connect( + lambda: webbrowser.open_new_tab( + "https://" + "discord.gg" + "/" + "PtPsb9Mpk6" + ) + ) + + self.newPretenseAction = QAction( + "&Generate a Pretense Campaign from the running campaign", self + ) + self.newPretenseAction.setIcon(QIcon(CONST.ICONS["Pretense_generate"])) + self.newPretenseAction.triggered.connect(self.newPretenseCampaign) + self.openLogsAction = QAction("Show &logs", self) self.openLogsAction.triggered.connect(self.showLogsDialog) @@ -234,6 +251,8 @@ class QLiberationWindow(QMainWindow): self.links_bar.addAction(self.openDiscordAction) self.links_bar.addAction(self.openGithubAction) self.links_bar.addAction(self.ukraineAction) + self.links_bar.addAction(self.pretenseLinkAction) + self.links_bar.addAction(self.newPretenseAction) self.actions_bar = self.addToolBar("Actions") self.actions_bar.addAction(self.openSettingsAction) @@ -303,6 +322,29 @@ class QLiberationWindow(QMainWindow): wizard.show() wizard.accepted.connect(lambda: self.onGameGenerated(wizard.generatedGame)) + def newPretenseCampaign(self): + output = persistency.mission_path_for("pretense_campaign.miz") + try: + PretenseMissionGenerator( + self.game, self.game.conditions.start_time + ).generate_miz(output) + except Exception as e: + now = datetime.now() + date_time = now.strftime("%Y-%d-%mT%H_%M_%S") + path = pre_pretense_backups_dir() + path.mkdir(parents=True, exist_ok=True) + tgt = path / f"pre-pretense-backup_{date_time}.retribution" + path /= f".pre-pretense-backup.retribution" + if path.exists(): + with open(path, "rb") as source: + with open(tgt, "wb") as target: + target.write(source.read()) + raise e + + title = "Pretense campaign generated" + msg = f"A Pretense campaign mission has been successfully generated in {output}" + QMessageBox.information(QApplication.focusWidget(), title, msg, QMessageBox.Ok) + def openFile(self): if self.game is not None and self.game.savepath: save_dir = self.game.savepath diff --git a/resources/campaigns/afghanistan_full.miz b/resources/campaigns/afghanistan_full.miz new file mode 100644 index 00000000..993a1741 Binary files /dev/null and b/resources/campaigns/afghanistan_full.miz differ diff --git a/resources/campaigns/afghanistan_full.yaml b/resources/campaigns/afghanistan_full.yaml new file mode 100644 index 00000000..bf38482b --- /dev/null +++ b/resources/campaigns/afghanistan_full.yaml @@ -0,0 +1,104 @@ +--- +name: Afghanistan - [Pretense] - Full map +theater: Afghanistan +authors: Colonel Akir Nakesh +recommended_player_faction: Bluefor Modern +recommended_enemy_faction: Insurgents +description: +

An Afghanistan full map campaign. The campaign is tuned for out-of-the-box Pretense generation compatibility and as such may be unbalanced for a standard campaign.

+miz: afghanistan_full.miz +performance: 3 +version: "10.7" +squadrons: + # Herat + 1: + - primary: Air Assault + aircraft: + - UH-1H Iroquois + - Mi-8MTV2 Hip + - primary: CAS + aircraft: + - AH-64D Apache Longbow + - Ka-50 Hokum III + - Ka-50 Hokum + - Mi-24P Hind-F + - primary: CAS + secondary: any + aircraft: + - A-10C Thunderbolt II (Suite 7) + - A-10C Thunderbolt II (Suite 3) + - A-10A Thunderbolt II + - Su-25 Frogfoot + - L-39ZA Albatros + - primary: SEAD + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F/A-18C Hornet (Lot 20) + - Su-25T Frogfoot + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-15C Eagle + - J-11A Flanker-L + - MiG-29S Fulcrum-C + - Su-27 Flanker-B + - Su-33 Flanker-D + - MiG-29A Fulcrum-A + - primary: AEW&C + aircraft: + - E-3A + - A-50 + - primary: Refueling + aircraft: + - KC-135 Stratotanker + - IL-78M + - primary: Transport + aircraft: + - C-130 + - An-26B + # Kandahar + 7: + - primary: Air Assault + aircraft: + - UH-1H Iroquois + - Mi-8MTV2 Hip + - primary: CAS + aircraft: + - AH-64D Apache Longbow + - Ka-50 Hokum III + - Ka-50 Hokum + - Mi-24P Hind-F + - primary: CAS + secondary: any + aircraft: + - A-10C Thunderbolt II (Suite 7) + - A-10C Thunderbolt II (Suite 3) + - A-10A Thunderbolt II + - Su-25 Frogfoot + - L-39ZA Albatros + - primary: SEAD + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F/A-18C Hornet (Lot 20) + - Su-25T Frogfoot + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-15C Eagle + - J-11A Flanker-L + - MiG-29S Fulcrum-C + - Su-27 Flanker-B + - Su-33 Flanker-D + - MiG-29A Fulcrum-A + - primary: AEW&C + aircraft: + - E-3A + - A-50 + - primary: Refueling + aircraft: + - KC-135 Stratotanker + - IL-78M + - primary: Transport + aircraft: + - C-130 + - An-26B diff --git a/resources/campaigns/marianas_full.miz b/resources/campaigns/marianas_full.miz new file mode 100644 index 00000000..5fbffae2 Binary files /dev/null and b/resources/campaigns/marianas_full.miz differ diff --git a/resources/campaigns/marianas_full.yaml b/resources/campaigns/marianas_full.yaml new file mode 100644 index 00000000..d0edb340 --- /dev/null +++ b/resources/campaigns/marianas_full.yaml @@ -0,0 +1,126 @@ +--- +name: Marianas - [Pretense] - Full map +theater: MarianaIslands +authors: Colonel Akir Nakesh +recommended_player_faction: Bluefor Modern +recommended_enemy_faction: China 2010 +description: +

A Marianas full map campaign. The campaign is tuned for out-of-the-box Pretense generation compatibility and as such will be unbalanced for a standard campaign.

+miz: marianas_full.miz +performance: 3 +version: "10.7" +squadrons: + #Andersen + 6: + - primary: Air Assault + aircraft: + - UH-1H Iroquois + - Mi-8MTV2 Hip + - primary: CAS + aircraft: + - AH-64D Apache Longbow + - Ka-50 Hokum III + - Ka-50 Hokum + - Mi-24P Hind-F + - primary: CAS + secondary: any + aircraft: + - A-10C Thunderbolt II (Suite 7) + - A-10C Thunderbolt II (Suite 3) + - A-10A Thunderbolt II + - Su-25 Frogfoot + - L-39ZA Albatros + - primary: SEAD + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F/A-18C Hornet (Lot 20) + - Su-25T Frogfoot + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-15C Eagle + - J-11A Flanker-L + - MiG-29S Fulcrum-C + - Su-27 Flanker-B + - Su-33 Flanker-D + - MiG-29A Fulcrum-A + - primary: AEW&C + aircraft: + - E-3A + - A-50 + - primary: Refueling + aircraft: + - KC-135 Stratotanker + - IL-78M + - primary: Transport + aircraft: + - C-130 + - An-26B + #Saipan + 2: + - primary: Air Assault + aircraft: + - UH-1H Iroquois + - Mi-8MTV2 Hip + - primary: CAS + aircraft: + - AH-64D Apache Longbow + - Ka-50 Hokum III + - Ka-50 Hokum + - Mi-24P Hind-F + - primary: CAS + secondary: any + aircraft: + - A-10C Thunderbolt II (Suite 7) + - A-10C Thunderbolt II (Suite 3) + - A-10A Thunderbolt II + - Su-25 Frogfoot + - L-39ZA Albatros + - primary: SEAD + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F/A-18C Hornet (Lot 20) + - Su-25T Frogfoot + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-15C Eagle + - J-11A Flanker-L + - MiG-29S Fulcrum-C + - Su-27 Flanker-B + - Su-33 Flanker-D + - MiG-29A Fulcrum-A + - primary: AEW&C + aircraft: + - E-3A + - A-50 + - primary: Refueling + aircraft: + - KC-135 Stratotanker + - IL-78M + - primary: Transport + aircraft: + - C-130 + - An-26B + Blue CVBG: + - primary: BARCAP + aircraft: + - F/A-18C Hornet (Lot 20) + - Su-33 Flanker-D + - primary: AEW&C + aircraft: + - E-2D Advanced Hawkeye + - primary: Refueling + aircraft: + - S-3B Tanker + Red CVBG: + - primary: BARCAP + aircraft: + - F/A-18C Hornet (Lot 20) + - Su-33 Flanker-D + - primary: AEW&C + aircraft: + - E-2D Advanced Hawkeye + - primary: Refueling + aircraft: + - S-3B Tanker \ No newline at end of file diff --git a/resources/campaigns/nevada_full.miz b/resources/campaigns/nevada_full.miz new file mode 100644 index 00000000..d8ec2972 Binary files /dev/null and b/resources/campaigns/nevada_full.miz differ diff --git a/resources/campaigns/nevada_full.yaml b/resources/campaigns/nevada_full.yaml new file mode 100644 index 00000000..a85d68fe --- /dev/null +++ b/resources/campaigns/nevada_full.yaml @@ -0,0 +1,104 @@ +--- +name: Nevada - [Pretense] - Full map +theater: Nevada +authors: Colonel Akir Nakesh +recommended_player_faction: Bluefor Modern +recommended_enemy_faction: USAF Aggressors +description: +

A Nevada full map campaign. The campaign is tuned for out-of-the-box Pretense generation compatibility and as such may be unbalanced for a standard campaign.

+miz: nevada_full.miz +performance: 3 +version: "10.7" +squadrons: + # Nellis + 4: + - primary: Air Assault + aircraft: + - UH-1H Iroquois + - Mi-8MTV2 Hip + - primary: CAS + aircraft: + - AH-64D Apache Longbow + - Ka-50 Hokum III + - Ka-50 Hokum + - Mi-24P Hind-F + - primary: CAS + secondary: any + aircraft: + - A-10C Thunderbolt II (Suite 7) + - A-10C Thunderbolt II (Suite 3) + - A-10A Thunderbolt II + - Su-25 Frogfoot + - L-39ZA Albatros + - primary: SEAD + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F/A-18C Hornet (Lot 20) + - Su-25T Frogfoot + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-15C Eagle + - J-11A Flanker-L + - MiG-29S Fulcrum-C + - Su-27 Flanker-B + - Su-33 Flanker-D + - MiG-29A Fulcrum-A + - primary: AEW&C + aircraft: + - E-3A + - A-50 + - primary: Refueling + aircraft: + - KC-135 Stratotanker + - IL-78M + - primary: Transport + aircraft: + - C-130 + - An-26B + # Tonopah Test Range + 18: + - primary: Air Assault + aircraft: + - UH-1H Iroquois + - Mi-8MTV2 Hip + - primary: CAS + aircraft: + - AH-64D Apache Longbow + - Ka-50 Hokum III + - Ka-50 Hokum + - Mi-24P Hind-F + - primary: CAS + secondary: any + aircraft: + - A-10C Thunderbolt II (Suite 7) + - A-10C Thunderbolt II (Suite 3) + - A-10A Thunderbolt II + - Su-25 Frogfoot + - L-39ZA Albatros + - primary: SEAD + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F/A-18C Hornet (Lot 20) + - Su-25T Frogfoot + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-15C Eagle + - J-11A Flanker-L + - MiG-29S Fulcrum-C + - Su-27 Flanker-B + - Su-33 Flanker-D + - MiG-29A Fulcrum-A + - primary: AEW&C + aircraft: + - E-3A + - A-50 + - primary: Refueling + aircraft: + - KC-135 Stratotanker + - IL-78M + - primary: Transport + aircraft: + - C-130 + - An-26B diff --git a/resources/campaigns/persian_gulf_full.miz b/resources/campaigns/persian_gulf_full.miz new file mode 100644 index 00000000..7345e5b7 Binary files /dev/null and b/resources/campaigns/persian_gulf_full.miz differ diff --git a/resources/campaigns/persian_gulf_full.yaml b/resources/campaigns/persian_gulf_full.yaml new file mode 100644 index 00000000..d3f18111 --- /dev/null +++ b/resources/campaigns/persian_gulf_full.yaml @@ -0,0 +1,132 @@ +--- +name: Persian Gulf - [Pretense] - Full map +theater: Persian Gulf +authors: Colonel Akir Nakesh +recommended_player_faction: Bluefor Modern +recommended_enemy_faction: Iran 2015 +description: +

A Persian Gulf full map campaign. The campaign is tuned for out-of-the-box Pretense generation compatibility and as such will be unbalanced for a standard campaign. Recommended settings for campaign generation: disable frontline smoke, disable CTLD, disable Skynet, set CVBG navmesh to Redfor.

+miz: persian_gulf_full.miz +performance: 3 +version: "10.7" +settings: + pretense_carrier_zones_navmesh: "Red navmesh" + perf_smoke_gen: false + plugins: + ctld: false + skynetiads: false +squadrons: + #Al Dhafra AFB + 4: + - primary: Air Assault + aircraft: + - UH-1H Iroquois + - Mi-8MTV2 Hip + - primary: CAS + aircraft: + - AH-64D Apache Longbow + - Ka-50 Hokum III + - Ka-50 Hokum + - Mi-24P Hind-F + - primary: CAS + secondary: any + aircraft: + - A-10C Thunderbolt II (Suite 7) + - A-10C Thunderbolt II (Suite 3) + - A-10A Thunderbolt II + - Su-25 Frogfoot + - L-39ZA Albatros + - primary: SEAD + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F/A-18C Hornet (Lot 20) + - Su-25T Frogfoot + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-15C Eagle + - J-11A Flanker-L + - MiG-29S Fulcrum-C + - Su-27 Flanker-B + - Su-33 Flanker-D + - MiG-29A Fulcrum-A + - primary: AEW&C + aircraft: + - E-3A + - A-50 + - primary: Refueling + aircraft: + - KC-135 Stratotanker + - IL-78M + - primary: Transport + aircraft: + - C-130 + - An-26B + #Shiraz Intl + 19: + - primary: Air Assault + aircraft: + - UH-1H Iroquois + - Mi-8MTV2 Hip + - primary: CAS + aircraft: + - AH-64D Apache Longbow + - Ka-50 Hokum III + - Ka-50 Hokum + - Mi-24P Hind-F + - primary: CAS + secondary: any + aircraft: + - A-10C Thunderbolt II (Suite 7) + - A-10C Thunderbolt II (Suite 3) + - A-10A Thunderbolt II + - Su-25 Frogfoot + - L-39ZA Albatros + - primary: SEAD + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F/A-18C Hornet (Lot 20) + - Su-25T Frogfoot + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-15C Eagle + - J-11A Flanker-L + - MiG-29S Fulcrum-C + - Su-27 Flanker-B + - Su-33 Flanker-D + - MiG-29A Fulcrum-A + - primary: AEW&C + aircraft: + - E-3A + - A-50 + - primary: Refueling + aircraft: + - KC-135 Stratotanker + - IL-78M + - primary: Transport + aircraft: + - C-130 + - An-26B + Blue CVBG: + - primary: BARCAP + aircraft: + - F/A-18C Hornet (Lot 20) + - Su-33 Flanker-D + - primary: AEW&C + aircraft: + - E-2D Advanced Hawkeye + - primary: Refueling + aircraft: + - S-3B Tanker + Red CVBG: + - primary: BARCAP + aircraft: + - F/A-18C Hornet (Lot 20) + - Su-33 Flanker-D + - primary: AEW&C + aircraft: + - E-2D Advanced Hawkeye + - primary: Refueling + aircraft: + - S-3B Tanker \ No newline at end of file diff --git a/resources/plugins/pretense/init_body_1.lua b/resources/plugins/pretense/init_body_1.lua new file mode 100644 index 00000000..4b18a0fd --- /dev/null +++ b/resources/plugins/pretense/init_body_1.lua @@ -0,0 +1,519 @@ + +end + +presets = { + upgrades = { + basic = { + tent = Preset:new({ + display = 'Tent', + cost = 1500, + type = 'upgrade', + template = "tent" + }), + comPost = Preset:new({ + display = 'Barracks', + cost = 1500, + type = 'upgrade', + template = "barracks" + }), + outpost = Preset:new({ + display = 'Outpost', + cost = 1500, + type = 'upgrade', + template = "outpost" + }), + artyBunker = Preset:new({ + display = 'Artillery Bunker', + cost = 2000, + type = 'upgrade', + template = "ammo-depot" + }) + }, + attack = { + ammoCache = Preset:new({ + display = 'Ammo Cache', + cost = 1500, + type = 'upgrade', + template = "ammo-cache" + }), + ammoDepot = Preset:new({ + display = 'Ammo Depot', + cost = 2000, + type = 'upgrade', + template = "ammo-depot" + }), + chemTank = Preset:new({ + display='Chemical Tank', + cost = 2000, + type ='upgrade', + template = "chem-tank" + }), + }, + supply = { + fuelCache = Preset:new({ + display = 'Fuel Cache', + cost = 1500, + type = 'upgrade', + template = "fuel-cache" + }), + fuelTank = Preset:new({ + display = 'Fuel Tank', + cost = 1500, + type = 'upgrade', + template = "fuel-tank-big" + }), + fuelTankFarp = Preset:new({ + display = 'Fuel Tank', + cost = 1500, + type = 'upgrade', + template = "fuel-tank-small" + }), + factory1 = Preset:new({ + display='Factory', + cost = 2000, + type ='upgrade', + income = 20, + template = "factory-1" + }), + factory2 = Preset:new({ + display='Factory', + cost = 2000, + type ='upgrade', + income = 20, + template = "factory-2" + }), + factoryTank = Preset:new({ + display='Storage Tank', + cost = 1500, + type ='upgrade', + income = 10, + template = "chem-tank" + }), + ammoDepot = Preset:new({ + display = 'Ammo Depot', + cost = 2000, + type = 'upgrade', + income = 40, + template = "ammo-depot" + }), + oilPump = Preset:new({ + display = 'Oil Pump', + cost = 1500, + type = 'upgrade', + income = 20, + template = "oil-pump" + }), + hangar = Preset:new({ + display = 'Hangar', + cost = 2000, + type = 'upgrade', + income = 30, + template = "hangar" + }), + excavator = Preset:new({ + display = 'Excavator', + cost = 2000, + type = 'upgrade', + income = 20, + template = "excavator" + }), + farm1 = Preset:new({ + display = 'Farm House', + cost = 2000, + type = 'upgrade', + income = 40, + template = "farm-house-1" + }), + farm2 = Preset:new({ + display = 'Farm House', + cost = 2000, + type = 'upgrade', + income = 40, + template = "farm-house-2" + }), + refinery1 = Preset:new({ + display='Refinery', + cost = 2000, + type ='upgrade', + income = 100, + template = "factory-1" + }), + powerplant1 = Preset:new({ + display='Power Plant', + cost = 1500, + type ='upgrade', + income = 25, + template = "factory-1" + }), + powerplant2 = Preset:new({ + display='Power Plant', + cost = 1500, + type ='upgrade', + income = 25, + template = "factory-2" + }), + antenna = Preset:new({ + display='Antenna', + cost = 1000, + type ='upgrade', + income = 10, + template = "antenna" + }), + hq = Preset:new({ + display='HQ Building', + cost = 2000, + type ='upgrade', + income = 50, + template = "tv-tower" + }), + }, + airdef = { + bunker = Preset:new({ + display = 'Excavator', + cost = 1500, + type = 'upgrade', + template = "excavator" + }), + comCenter = Preset:new({ + display = 'Command Center', + cost = 12500, + type = 'upgrade', + template = "command-center" + }) + } + }, + defenses = { + red = { + infantry = Preset:new({ + display = 'Infantry', + cost=2000, + type='defense', + template='infantry-red', + }), + artillery = Preset:new({ + display = 'Artillery', + cost=2500, + type='defense', + template='artillery-red', + }), + shorad = Preset:new({ + display = 'SHORAD', + cost=2500, + type='defense', + template='shorad-red', + }), + sa2 = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='sa2-red', + }), + sa10 = Preset:new({ + display = 'SAM', + cost=30000, + type='defense', + template='sa10-red', + }), + sa5 = Preset:new({ + display = 'SAM', + cost=20000, + type='defense', + template='sa5-red', + }), + sa3 = Preset:new({ + display = 'SAM', + cost=4000, + type='defense', + template='sa3-red', + }), + sa6 = Preset:new({ + display = 'SAM', + cost=6000, + type='defense', + template='sa6-red', + }), + sa11 = Preset:new({ + display = 'SAM', + cost=10000, + type='defense', + template='sa11-red', + }), + hawk = Preset:new({ + display = 'SAM', + cost=6000, + type='defense', + template='hawk-red', + }), + patriot = Preset:new({ + display = 'SAM', + cost=30000, + type='defense', + template='patriot-red', + }), + nasamsb = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='nasamsb-red', + }), + nasamsc = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='nasamsc-red', + }), + rapier = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='rapier-red', + }), + roland = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='roland-red', + }), + irondome = Preset:new({ + display = 'SAM', + cost=20000, + type='defense', + template='irondome-red', + }), + davidsling = Preset:new({ + display = 'SAM', + cost=30000, + type='defense', + template='davidsling-red', + }), + hq7 = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='hq7-red', + }) + }, + blue = { + infantry = Preset:new({ + display = 'Infantry', + cost=2000, + type='defense', + template='infantry-blue', + }), + artillery = Preset:new({ + display = 'Artillery', + cost=2500, + type='defense', + template='artillery-blue', + }), + shorad = Preset:new({ + display = 'SHORAD', + cost=2500, + type='defense', + template='shorad-blue', + }), + sa2 = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='sa2-blue', + }), + sa10 = Preset:new({ + display = 'SAM', + cost=30000, + type='defense', + template='sa10-blue', + }), + sa5 = Preset:new({ + display = 'SAM', + cost=20000, + type='defense', + template='sa5-blue', + }), + sa3 = Preset:new({ + display = 'SAM', + cost=4000, + type='defense', + template='sa3-blue', + }), + sa6 = Preset:new({ + display = 'SAM', + cost=6000, + type='defense', + template='sa6-blue', + }), + sa11 = Preset:new({ + display = 'SAM', + cost=10000, + type='defense', + template='sa11-blue', + }), + hawk = Preset:new({ + display = 'SAM', + cost=6000, + type='defense', + template='hawk-blue', + }), + patriot = Preset:new({ + display = 'SAM', + cost=30000, + type='defense', + template='patriot-blue', + }), + nasamsb = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='nasamsb-blue', + }), + nasamsc = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='nasamsc-blue', + }), + rapier = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='rapier-blue', + }), + roland = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='roland-blue', + }), + irondome = Preset:new({ + display = 'SAM', + cost=20000, + type='defense', + template='irondome-blue', + }), + davidsling = Preset:new({ + display = 'SAM', + cost=30000, + type='defense', + template='davidsling-blue', + }), + hq7 = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='hq7-blue', + }) + } + }, + missions = { + supply = { + convoy = Preset:new({ + display = 'Supply convoy', + cost = 4000, + type = 'mission', + missionType = ZoneCommand.missionTypes.supply_convoy + }), + convoy_escorted = Preset:new({ + display = 'Supply convoy', + cost = 3000, + type = 'mission', + missionType = ZoneCommand.missionTypes.supply_convoy + }), + helo = Preset:new({ + display = 'Supply helicopter', + cost = 2500, + type='mission', + missionType = ZoneCommand.missionTypes.supply_air + }), + transfer = Preset:new({ + display = 'Supply transfer', + cost = 1000, + type='mission', + missionType = ZoneCommand.missionTypes.supply_transfer + }) + }, + attack = { + surface = Preset:new({ + display = 'Ground assault', + cost = 100, + type = 'mission', + missionType = ZoneCommand.missionTypes.assault, + }), + cas = Preset:new({ + display = 'CAS', + cost = 200, + type='mission', + missionType = ZoneCommand.missionTypes.cas + }), + bai = Preset:new({ + display = 'BAI', + cost = 200, + type='mission', + missionType = ZoneCommand.missionTypes.bai + }), + strike = Preset:new({ + display = 'Strike', + cost = 300, + type='mission', + missionType = ZoneCommand.missionTypes.strike + }), + sead = Preset:new({ + display = 'SEAD', + cost = 200, + type='mission', + missionType = ZoneCommand.missionTypes.sead + }), + helo = Preset:new({ + display = 'CAS', + cost = 100, + type='mission', + missionType = ZoneCommand.missionTypes.cas_helo + }) + }, + patrol={ + aircraft = Preset:new({ + display= "Patrol", + cost = 100, + type='mission', + missionType = ZoneCommand.missionTypes.patrol + }) + }, + support ={ + awacs = Preset:new({ + display= "AWACS", + cost = 300, + type='mission', + bias='5', + missionType = ZoneCommand.missionTypes.awacs + }), + tanker = Preset:new({ + display= "Tanker", + cost = 200, + type='mission', + bias='2', + missionType = ZoneCommand.missionTypes.tanker + }) + } + }, + special = { + red = { + infantry = Preset:new({ + display = 'Infantry', + cost=-1, + type='defense', + template='defense-red', + }), + }, + blue = { + infantry = Preset:new({ + display = 'Infantry', + cost=-1, + type='defense', + template='defense-blue', + }) + } + } +} + +zones = {} +do + diff --git a/resources/plugins/pretense/init_body_2.lua b/resources/plugins/pretense/init_body_2.lua new file mode 100644 index 00000000..3f5068a8 --- /dev/null +++ b/resources/plugins/pretense/init_body_2.lua @@ -0,0 +1,32 @@ + +end + +ZoneCommand.setNeighbours(cm) + +bm = BattlefieldManager:new() + +mc = MarkerCommands:new() + +pt = PlayerTracker:new(mc) + +mt = MissionTracker:new(pt, mc) + +st = SquadTracker:new() + +ct = CSARTracker:new() + +pl = PlayerLogistics:new(mt, pt, st, ct) + +gci = GCI:new(2) + +gm = GroupMonitor:new(cm) +ZoneCommand.groupMonitor = gm + +-- PlayerLogistics:registerSquadGroup(squadType, groupname, weight,cost,jobtime,extracttime, squadSize) +pl:registerSquadGroup(PlayerLogistics.infantryTypes.capture, 'capture-squad', 700, 200, 60, 60*30, 4) +pl:registerSquadGroup(PlayerLogistics.infantryTypes.sabotage, 'sabotage-squad', 800, 500, 60*5, 60*30, 4) +pl:registerSquadGroup(PlayerLogistics.infantryTypes.ambush, 'ambush-squad', 900, 300, 60*20, 60*30, 5) +pl:registerSquadGroup(PlayerLogistics.infantryTypes.engineer, 'engineer-squad', 200, 1000,60, 60*30, 2) +pl:registerSquadGroup(PlayerLogistics.infantryTypes.manpads, 'manpads-squad', 900, 500, 60*20, 60*30, 5) +pl:registerSquadGroup(PlayerLogistics.infantryTypes.spy, 'spy-squad', 100, 300, 60*10, 60*30, 1) +pl:registerSquadGroup(PlayerLogistics.infantryTypes.rapier, 'rapier-squad', 1200,2000,60*60, 60*30, 8) diff --git a/resources/plugins/pretense/init_body_3.lua b/resources/plugins/pretense/init_body_3.lua new file mode 100644 index 00000000..9994dce9 --- /dev/null +++ b/resources/plugins/pretense/init_body_3.lua @@ -0,0 +1,62 @@ + +pm = PersistenceManager:new(savefile, gm, st, ct, pl) +pm:load() + +if pm:canRestore() then + pm:restoreZones() + pm:restoreAIMissions() + pm:restoreBattlefield() + pm:restoreCsar() + pm:restoreSquads() +else + --initial states + Starter.start(zones) +end + +timer.scheduleFunction(function(param, time) + pm:save() + env.info("Mission state saved") + return time+60 +end, zones, timer.getTime()+60) + + +--make sure support units are present where needed +ensureSpawn = { + ['golf-farp-suport'] = zones.golf, + ['november-farp-suport'] = zones.november, + ['tango-farp-suport'] = zones.tango, + ['sierra-farp-suport'] = zones.sierra, + ['cherkessk-farp-suport'] = zones.cherkessk, + ['unal-farp-suport'] = zones.unal, + ['tyrnyauz-farp-suport'] = zones.tyrnyauz +} + +for grname, zn in pairs(ensureSpawn) do + local g = Group.getByName(grname) + if g then g:destroy() end +end + +timer.scheduleFunction(function(param, time) + + for grname, zn in pairs(ensureSpawn) do + local g = Group.getByName(grname) + if zn.side == 2 then + if not g then + local err, msg = pcall(mist.respawnGroup,grname,true) + if not err then + env.info("ERROR spawning "..grname) + env.info(msg) + end + end + else + if g then g:destroy() end + end + end + + return time+30 +end, {}, timer.getTime()+30) + + +--supply injection + + diff --git a/resources/plugins/pretense/init_footer.lua b/resources/plugins/pretense/init_footer.lua new file mode 100644 index 00000000..455520b2 --- /dev/null +++ b/resources/plugins/pretense/init_footer.lua @@ -0,0 +1,141 @@ + +supplyPointRegistry = { + blue = {}, + red = {} +} + +for i,v in ipairs(blueSupply) do + local g = Group.getByName(v) + if g then + supplyPointRegistry.blue[v] = g:getUnit(1):getPoint() + end +end + +for i,v in ipairs(redSupply) do + local g = Group.getByName(v) + if g then + supplyPointRegistry.red[v] = g:getUnit(1):getPoint() + end +end + +offmapSupplyRegistry = {} +timer.scheduleFunction(function(param, time) + local availableBlue = {} + for i,v in ipairs(param.blue) do + if offmapSupplyRegistry[v] == nil then + table.insert(availableBlue, v) + end + end + + local availableRed = {} + for i,v in ipairs(param.red) do + if offmapSupplyRegistry[v] == nil then + table.insert(availableRed, v) + end + end + + local redtargets = {} + local bluetargets = {} + for _, zn in ipairs(param.offmapZones) do + if zn:needsSupplies(3000) then + local isOnRoute = false + for _,data in pairs(offmapSupplyRegistry) do + if data.zone.name == zn.name then + isOnRoute = true + break + end + end + if not isOnRoute then + if zn.side == 1 then + table.insert(redtargets, zn) + elseif zn.side == 2 then + table.insert(bluetargets, zn) + end + end + end + end + + if #availableRed > 0 and #redtargets > 0 then + local zn = redtargets[math.random(1,#redtargets)] + + local red = nil + local minD = 999999999 + for i,v in ipairs(availableRed) do + local d = mist.utils.get2DDist(zn.zone.point, supplyPointRegistry.red[v]) + if d < minD then + red = v + minD = d + end + end + + if not red then red = availableRed[math.random(1,#availableRed)] end + + local gr = red + red = nil + mist.respawnGroup(gr, true) + offmapSupplyRegistry[gr] = {zone = zn, assigned = timer.getAbsTime()} + env.info(gr..' was deployed') + timer.scheduleFunction(function(param,time) + local g = Group.getByName(param.group) + TaskExtensions.landAtAirfield(g, param.target.zone.point) + env.info(param.group..' going to '..param.target.name) + end, {group=gr, target=zn}, timer.getTime()+2) + end + + if #availableBlue > 0 and #bluetargets>0 then + local zn = bluetargets[math.random(1,#bluetargets)] + + local blue = nil + local minD = 999999999 + for i,v in ipairs(availableBlue) do + local d = mist.utils.get2DDist(zn.zone.point, supplyPointRegistry.blue[v]) + if d < minD then + blue = v + minD = d + end + end + + if not blue then blue = availableBlue[math.random(1,#availableBlue)] end + + local gr = blue + blue = nil + mist.respawnGroup(gr, true) + offmapSupplyRegistry[gr] = {zone = zn, assigned = timer.getAbsTime()} + env.info(gr..' was deployed') + timer.scheduleFunction(function(param,time) + local g = Group.getByName(param.group) + TaskExtensions.landAtAirfield(g, param.target.zone.point) + env.info(param.group..' going to '..param.target.name) + end, {group=gr, target=zn}, timer.getTime()+2) + end + + return time+(60*5) +end, {blue = blueSupply, red = redSupply, offmapZones = offmapZones}, timer.getTime()+60) + + + +timer.scheduleFunction(function(param, time) + + for groupname,data in pairs(offmapSupplyRegistry) do + local gr = Group.getByName(groupname) + if not gr then + offmapSupplyRegistry[groupname] = nil + env.info(groupname..' was destroyed') + end + + if gr and ((timer.getAbsTime() - data.assigned) > (60*60)) then + gr:destroy() + offmapSupplyRegistry[groupname] = nil + env.info(groupname..' despawned due to being alive for too long') + end + + if gr and Utils.allGroupIsLanded(gr) and Utils.someOfGroupInZone(gr, data.zone.name) then + data.zone:addResource(15000) + gr:destroy() + offmapSupplyRegistry[groupname] = nil + env.info(groupname..' landed at '..data.zone.name..' and delivered 15000 resources') + end + end + + return time+180 +end, {}, timer.getTime()+180) \ No newline at end of file diff --git a/resources/plugins/pretense/init_header.lua b/resources/plugins/pretense/init_header.lua new file mode 100644 index 00000000..3b9cbbf5 --- /dev/null +++ b/resources/plugins/pretense/init_header.lua @@ -0,0 +1,12 @@ + + +if lfs then + local dir = lfs.writedir()..'Missions/Saves/' + lfs.mkdir(dir) + savefile = dir..savefile + env.info('Pretense - Save file path: '..savefile) +end + + +do + diff --git a/resources/plugins/pretense/pretense_compiled.lua b/resources/plugins/pretense/pretense_compiled.lua new file mode 100644 index 00000000..34e2529c --- /dev/null +++ b/resources/plugins/pretense/pretense_compiled.lua @@ -0,0 +1,15862 @@ +--[[ +Pretense Dynamic Mission Engine +## Description: + +Pretense Dynamic Mission Engine (PDME) is a the heart and soul of the Pretense missions. +You are allowed to use and modify this script for personal or private use. +Please do not share modified versions of this script. +Please do not reupload missions that use this script. +Please do not charge money for access to missions using this script. + +## Links: + +ED Forums Post: + +Pretense Manual: + +If you'd like to buy me a beer: + +Makes use of Mission scripting tools (Mist): + +@script PDME +@author Dzsekeb + +]]-- + +-----------------[[ GroupCorrection.lua ]]----------------- + +Group.getByNameBase = Group.getByName + +function Group.getByName(name) + local g = Group.getByNameBase(name) + if not g then return nil end + if g:getSize() == 0 then return nil end + return g +end + +-----------------[[ END OF GroupCorrection.lua ]]----------------- + + + +-----------------[[ DependencyManager.lua ]]----------------- + +DependencyManager = {} + +do + DependencyManager.dependencies = {} + + function DependencyManager.register(name, dependency) + DependencyManager.dependencies[name] = dependency + env.info("DependencyManager - "..name.." registered") + end + + function DependencyManager.get(name) + return DependencyManager.dependencies[name] + end +end + +-----------------[[ END OF DependencyManager.lua ]]----------------- + + + +-----------------[[ Config.lua ]]----------------- + +Config = Config or {} +Config.lossCompensation = Config.lossCompensation or 1.1 -- gives advantage to the side with less zones. Set to 0 to disable +Config.randomBoost = Config.randomBoost or 0.0004 -- adds a random factor to build speeds that changes every 30 minutes, set to 0 to disable +Config.buildSpeed = Config.buildSpeed or 10 -- structure and defense build speed +Config.supplyBuildSpeed = Config.supplyBuildSpeed or 85 -- supply helicopters and convoys build speed +Config.missionBuildSpeedReduction = Config.missionBuildSpeedReduction or 0.12 -- reduction of build speed in case of ai missions +Config.maxDistFromFront = Config.maxDistFromFront or 129640 -- max distance in meters from front after which zone is forced into low activity state (export mode) + +if Config.restrictMissionAcceptance == nil then Config.restrictMissionAcceptance = true end -- if set to true, missions can only be accepted while landed inside friendly zones + +Config.missions = Config.missions or {} +Config.missionBoardSize = Config.missionBoardSize or 15 + +Config.carrierSpawnCost = Config.carrierSpawnCost or 500 -- resource cost for carrier when players take off, set to 0 to disable restriction +Config.zoneSpawnCost = Config.zoneSpawnCost or 500 -- resource cost for zones when players take off, set to 0 to disable restriction + +-----------------[[ END OF Config.lua ]]----------------- + + + +-----------------[[ Utils.lua ]]----------------- + +Utils = {} +do + local JSON = (loadfile('Scripts/JSON.lua'))() + + function Utils.getPointOnSurface(point) + return {x = point.x, y = land.getHeight({x = point.x, y = point.z}), z= point.z} + end + + function Utils.getTableSize(tbl) + local cnt = 0 + for i,v in pairs(tbl) do cnt=cnt+1 end + return cnt + end + + function Utils.isInArray(value, array) + for _,v in ipairs(array) do + if value == v then + return true + end + end + end + + Utils.cache = {} + Utils.cache.groups = {} + function Utils.getOriginalGroup(groupName) + if Utils.cache.groups[groupName] then + return Utils.cache.groups[groupName] + end + + for _,coalition in pairs(env.mission.coalition) do + for _,country in pairs(coalition.country) do + local tocheck = {} + table.insert(tocheck, country.plane) + table.insert(tocheck, country.helicopter) + table.insert(tocheck, country.ship) + table.insert(tocheck, country.vehicle) + table.insert(tocheck, country.static) + + for _, checkGroup in ipairs(tocheck) do + for _,item in pairs(checkGroup.group) do + Utils.cache.groups[item.name] = item + if item.name == groupName then + return item + end + end + end + end + end + end + + function Utils.getBearing(fromvec, tovec) + local fx = fromvec.x + local fy = fromvec.z + + local tx = tovec.x + local ty = tovec.z + + local brg = math.atan2(ty - fy, tx - fx) + + + if brg < 0 then + brg = brg + 2 * math.pi + end + + brg = brg * 180 / math.pi + + + return brg + end + + function Utils.getHeadingDiff(heading1, heading2) -- heading1 + result == heading2 + local diff = heading1 - heading2 + local absDiff = math.abs(diff) + local complementaryAngle = 360 - absDiff + + if absDiff <= 180 then + return -diff + elseif heading1 > heading2 then + return complementaryAngle + else + return -complementaryAngle + end + end + + function Utils.getAGL(object) + local pt = object:getPoint() + return pt.y - land.getHeight({ x = pt.x, y = pt.z }) + end + + function Utils.round(number) + return math.floor(number+0.5) + end + + function Utils.isLanded(unit, ignorespeed) + --return (Utils.getAGL(unit)<5 and mist.vec.mag(unit:getVelocity())<0.10) + + if ignorespeed then + return not unit:inAir() + else + return (not unit:inAir() and mist.vec.mag(unit:getVelocity())<1) + end + end + + function Utils.getEnemy(ofside) + if ofside == 1 then return 2 end + if ofside == 2 then return 1 end + end + + function Utils.isGroupActive(group) + if group and group:getSize()>0 and group:getController():hasTask() then + return not Utils.allGroupIsLanded(group, true) + else + return false + end + end + + function Utils.isInAir(unit) + --return Utils.getAGL(unit)>5 + return unit:inAir() + end + + function Utils.isInZone(unit, zonename) + local zn = CustomZone:getByName(zonename) + if zn then + return zn:isInside(unit:getPosition().p) + end + + return false + end + + function Utils.isInCircle(point, center, radius) + local dist = mist.utils.get2DDist(point, center) + return dist0 then + table.insert(zns, v) + end + end + end + end + + if includeCarriers then + for i,v in pairs(CarrierCommand.getAllCarriers()) do + if not targetside or Utils.isInArray(v.side,targetside) then + table.insert(zns, v) + end + end + end + + if #zns == 0 then return false end + + table.sort(zns, function(a,b) return a.name < b.name end) + + local executeAction = function(act, params) + local err = act(params) + if not err then + missionCommands.removeItemForGroup(params.groupid, params.menu) + end + end + + local menu = missionCommands.addSubMenuForGroup(groupid, name) + local sub1 = nil + + local count = 0 + for i,v in ipairs(zns) do + count = count + 1 + if count<10 then + missionCommands.addCommandForGroup(groupid, v.name, menu, executeAction, action, {zone = v, menu=menu, groupid=groupid, data=data}) + elseif count==10 then + sub1 = missionCommands.addSubMenuForGroup(groupid, "More", menu) + missionCommands.addCommandForGroup(groupid, v.name, sub1, executeAction, action, {zone = v, menu=menu, groupid=groupid, data=data}) + elseif count%9==1 then + sub1 = missionCommands.addSubMenuForGroup(groupid, "More", sub1) + missionCommands.addCommandForGroup(groupid, v.name, sub1, executeAction, action, {zone = v, menu=menu, groupid=groupid, data=data}) + else + missionCommands.addCommandForGroup(groupid, v.name, sub1, executeAction, action, {zone = v, menu=menu, groupid=groupid, data=data}) + end + end + + return menu + end +end + +-----------------[[ END OF MenuRegistry.lua ]]----------------- + + + +-----------------[[ CustomZone.lua ]]----------------- + + +CustomZone = {} +do + function CustomZone:getByName(name) + local obj = {} + obj.name = name + + local zd = nil + for _,v in ipairs(env.mission.triggers.zones) do + if v.name == name then + zd = v + break + end + end + + if not zd then + return nil + end + + obj.type = zd.type -- 2 == quad, 0 == circle + if obj.type == 2 then + obj.vertices = {} + for _,v in ipairs(zd.verticies) do + local vertex = { + x = v.x, + y = 0, + z = v.y + } + table.insert(obj.vertices, vertex) + end + end + + obj.radius = zd.radius + obj.point = { + x = zd.x, + y = 0, + z = zd.y + } + + setmetatable(obj, self) + self.__index = self + return obj + end + + function CustomZone:isQuad() + return self.type==2 + end + + function CustomZone:isCircle() + return self.type==0 + end + + function CustomZone:isInside(point) + if self:isCircle() then + local dist = mist.utils.get2DDist(point, self.point) + return dist 1 then + trigger.action.outText('WARNING: group '..groupname..' has '..size..' units. Logistics will only function for group leader', 10) + end + + local cargomenu = missionCommands.addSubMenuForGroup(groupid, 'Logistics') + if logistics.supplies then + local supplyMenu = missionCommands.addSubMenuForGroup(groupid, 'Supplies', cargomenu) + local loadMenu = missionCommands.addSubMenuForGroup(groupid, 'Load', supplyMenu) + missionCommands.addCommandForGroup(groupid, 'Load 100 supplies', loadMenu, Utils.log(context.loadSupplies), context, {group=groupname, amount=100}) + missionCommands.addCommandForGroup(groupid, 'Load 500 supplies', loadMenu, Utils.log(context.loadSupplies), context, {group=groupname, amount=500}) + missionCommands.addCommandForGroup(groupid, 'Load 1000 supplies', loadMenu, Utils.log(context.loadSupplies), context, {group=groupname, amount=1000}) + missionCommands.addCommandForGroup(groupid, 'Load 2000 supplies', loadMenu, Utils.log(context.loadSupplies), context, {group=groupname, amount=2000}) + missionCommands.addCommandForGroup(groupid, 'Load 5000 supplies', loadMenu, Utils.log(context.loadSupplies), context, {group=groupname, amount=5000}) + + local unloadMenu = missionCommands.addSubMenuForGroup(groupid, 'Unload', supplyMenu) + missionCommands.addCommandForGroup(groupid, 'Unload 100 supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=100}) + missionCommands.addCommandForGroup(groupid, 'Unload 500 supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=500}) + missionCommands.addCommandForGroup(groupid, 'Unload 1000 supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=1000}) + missionCommands.addCommandForGroup(groupid, 'Unload 2000 supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=2000}) + missionCommands.addCommandForGroup(groupid, 'Unload 5000 supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=5000}) + missionCommands.addCommandForGroup(groupid, 'Unload all supplies', unloadMenu, Utils.log(context.unloadSupplies), context, {group=groupname, amount=9999999}) + end + + local sqs = {} + for sqType,_ in pairs(context.registeredSquadGroups) do + table.insert(sqs,sqType) + end + table.sort(sqs) + + if logistics.personCapacity then + local infMenu = missionCommands.addSubMenuForGroup(groupid, 'Infantry', cargomenu) + + local loadInfMenu = missionCommands.addSubMenuForGroup(groupid, 'Load', infMenu) + for _,sqType in ipairs(sqs) do + local menuName = 'Load '..PlayerLogistics.getInfantryName(sqType) + missionCommands.addCommandForGroup(groupid, menuName, loadInfMenu, Utils.log(context.loadInfantry), context, {group=groupname, type=sqType}) + end + + local unloadInfMenu = missionCommands.addSubMenuForGroup(groupid, 'Unload', infMenu) + for _,sqType in ipairs(sqs) do + local menuName = 'Unload '..PlayerLogistics.getInfantryName(sqType) + missionCommands.addCommandForGroup(groupid, menuName, unloadInfMenu, Utils.log(context.unloadInfantry), context, {group=groupname, type=sqType}) + end + missionCommands.addCommandForGroup(groupid, 'Unload Extracted squad', unloadInfMenu, Utils.log(context.unloadInfantry), context, {group=groupname, type=PlayerLogistics.infantryTypes.extractable}) + + missionCommands.addCommandForGroup(groupid, 'Extract squad', infMenu, Utils.log(context.extractSquad), context, groupname) + missionCommands.addCommandForGroup(groupid, 'Unload all', infMenu, Utils.log(context.unloadInfantry), context, {group=groupname}) + + local csarMenu = missionCommands.addSubMenuForGroup(groupid, 'CSAR', cargomenu) + missionCommands.addCommandForGroup(groupid, 'Show info (closest)', csarMenu, Utils.log(context.showPilot), context, groupname) + missionCommands.addCommandForGroup(groupid, 'Smoke marker (closest)', csarMenu, Utils.log(context.smokePilot), context, groupname) + missionCommands.addCommandForGroup(groupid, 'Flare (closest)', csarMenu, Utils.log(context.flarePilot), context, groupname) + missionCommands.addCommandForGroup(groupid, 'Extract pilot', csarMenu, Utils.log(context.extractPilot), context, groupname) + missionCommands.addCommandForGroup(groupid, 'Unload pilots', csarMenu, Utils.log(context.unloadPilots), context, groupname) + end + + missionCommands.addCommandForGroup(groupid, 'Cargo status', cargomenu, Utils.log(context.cargoStatus), context, groupname) + missionCommands.addCommandForGroup(groupid, 'Unload Everything', cargomenu, Utils.log(context.unloadAll), context, groupname) + + if unitType == 'Hercules' then + local loadmasterMenu = missionCommands.addSubMenuForGroup(groupid, 'Loadmaster', cargomenu) + + for _,sqType in ipairs(sqs) do + local menuName = 'Prepare '..PlayerLogistics.getInfantryName(sqType) + missionCommands.addCommandForGroup(groupid, menuName, loadmasterMenu, Utils.log(context.hercPrepareDrop), context, {group=groupname, type=sqType}) + end + + missionCommands.addCommandForGroup(groupid, 'Prepare Supplies', loadmasterMenu, Utils.log(context.hercPrepareDrop), context, {group=groupname, type='supplies'}) + end + + + context.groupMenus[groupid] = cargomenu + end + + if context.carriedCargo[groupid] then + context.carriedCargo[groupid] = 0 + end + + if context.carriedInfantry[groupid] then + context.carriedInfantry[groupid] = {} + end + + if context.carriedPilots[groupid] then + context.carriedPilots[groupid] = {} + end + + if context.lastLoaded[groupid] then + context.lastLoaded[groupid] = nil + end + + if context.hercPreparedDrops[groupid] then + context.hercPreparedDrops[groupid] = nil + end + end + end + elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + + if context.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid]) + context.groupMenus[groupid] = nil + end + end + end + end, self) + + local ev = {} + ev.context = self + function ev:onEvent(event) + if event.id == world.event.S_EVENT_SHOT and event.initiator and event.initiator:isExist() then + local unitName = event.initiator:getName() + local groupId = event.initiator:getGroup():getID() + local name = event.weapon:getDesc().typeName + if name == 'weapons.bombs.Generic Crate [20000lb]' then + local prepared = self.context.hercPreparedDrops[groupId] + + if not prepared then + prepared = 'supplies' + + if self.context.carriedInfantry[groupId] then + for _,v in ipairs(self.context.carriedInfantry[groupId]) do + if v.type ~= PlayerLogistics.infantryTypes.extractable then + prepared = v.type + break + end + end + end + + env.info('PlayerLogistics - Hercules - auto preparing '..prepared) + end + + if prepared then + if prepared == 'supplies' then + env.info('PlayerLogistics - Hercules - supplies getting dropped') + local carried = self.context.carriedCargo[groupId] + local amount = 0 + if carried and carried > 0 then + amount = 9000 + if carried < amount then + amount = carried + end + end + + if amount > 0 then + self.context.carriedCargo[groupId] = math.max(0,self.context.carriedCargo[groupId] - amount) + if not self.context.hercTracker.cargos[unitName] then + self.context.hercTracker.cargos[unitName] = {} + end + + table.insert(self.context.hercTracker.cargos[unitName],{ + object = event.weapon, + supply = amount, + lastLoaded = self.context.lastLoaded[groupId], + unit = event.initiator + }) + + env.info('PlayerLogistics - Hercules - '..unitName..' deployed crate with '..amount..' supplies') + self.context:processHercCargos(unitName) + self.context.hercPreparedDrops[groupId] = nil + trigger.action.outTextForUnit(event.initiator:getID(), 'Crate with '..amount..' supplies deployed', 10) + else + trigger.action.outTextForUnit(event.initiator:getID(), 'Empty crate deployed', 10) + end + else + env.info('PlayerLogistics - Hercules - searching for prepared infantry') + local toDrop = nil + local remaining = {} + if self.context.carriedInfantry[groupId] then + for _,v in ipairs(self.context.carriedInfantry[groupId]) do + if v.type == prepared and toDrop == nil then + toDrop = v + else + table.insert(remaining, v) + end + end + end + + + if toDrop then + env.info('PlayerLogistics - Hercules - dropping '..toDrop.type) + if not self.context.hercTracker.cargos[unitName] then + self.context.hercTracker.cargos[unitName] = {} + end + + table.insert(self.context.hercTracker.cargos[unitName],{ + object = event.weapon, + squad = toDrop, + lastLoaded = self.context.lastLoaded[groupId], + unit = event.initiator + }) + + env.info('PlayerLogistics - Hercules - '..unitName..' deployed crate with '..toDrop.type) + self.context:processHercCargos(unitName) + self.context.hercPreparedDrops[groupId] = nil + + local squadName = PlayerLogistics.getInfantryName(prepared) + trigger.action.outTextForUnit(event.initiator:getID(), squadName..' crate deployed.', 10) + self.context.carriedInfantry[groupId] = remaining + local weight = self.context:getCarriedPersonWeight(event.initiator:getGroup():getName()) + trigger.action.setUnitInternalCargo(event.initiator:getName(), weight) + else + trigger.action.outTextForUnit(event.initiator:getID(), 'Empty crate deployed', 10) + end + end + else + trigger.action.outTextForUnit(event.initiator:getID(), 'Empty crate deployed', 10) + end + end + end + end + + world.addEventHandler(ev) + end + + function PlayerLogistics:processHercCargos(unitName) + if not self.hercTracker.cargoCheckFunctions[unitName] then + env.info('PlayerLogistics - Hercules - start tracking cargos of '..unitName) + self.hercTracker.cargoCheckFunctions[unitName] = timer.scheduleFunction(function(params, time) + local reschedule = params.context:checkHercCargo(params.unitName, time) + if not reschedule then + params.context.hercTracker.cargoCheckFunctions[params.unitName] = nil + env.info('PlayerLogistics - Hercules - stopped tracking cargos of '..unitName) + end + + return reschedule + end, {unitName=unitName, context = self}, timer.getTime() + 0.1) + end + end + + function PlayerLogistics:checkHercCargo(unitName, time) + local cargos = self.hercTracker.cargos[unitName] + if cargos and #cargos > 0 then + local remaining = {} + for _,cargo in ipairs(cargos) do + if cargo.object and cargo.object:isExist() then + local alt = Utils.getAGL(cargo.object) + if alt < 5 then + self:deliverHercCargo(cargo) + else + table.insert(remaining, cargo) + end + else + env.info('PlayerLogistics - Hercules - cargo crashed '..tostring(cargo.supply)..' '..tostring(cargo.squad)) + if cargo.squad then + env.info('PlayerLogistics - Hercules - squad crashed '..tostring(cargo.squad.type)) + end + + if cargo.unit and cargo.unit:isExist() then + if cargo.squad then + local squadName = PlayerLogistics.getInfantryName(cargo.squad.type) + trigger.action.outTextForUnit(cargo.unit:getID(), 'Cargo drop of '..cargo.unit:getPlayerName()..' with '..squadName..' crashed', 10) + elseif cargo.supply then + trigger.action.outTextForUnit(cargo.unit:getID(), 'Cargo drop of '..cargo.unit:getPlayerName()..' with '..cargo.supply..' supplies crashed', 10) + end + end + end + end + + if #remaining > 0 then + self.hercTracker.cargos[unitName] = remaining + return time + 0.1 + end + end + end + + function PlayerLogistics:deliverHercCargo(cargo) + if cargo.object and cargo.object:isExist() then + if cargo.supply then + local zone = ZoneCommand.getZoneOfWeapon(cargo.object) + if zone then + zone:addResource(cargo.supply) + env.info('PlayerLogistics - Hercules - '..cargo.supply..' delivered to '..zone.name) + + self:awardSupplyXP(cargo.lastLoaded, zone, cargo.unit, cargo.supply) + end + elseif cargo.squad then + local pos = Utils.getPointOnSurface(cargo.object:getPoint()) + pos.y = pos.z + pos.z = nil + local surface = land.getSurfaceType(pos) + if surface == land.SurfaceType.LAND or surface == land.SurfaceType.ROAD or surface == land.SurfaceType.RUNWAY then + local zn = ZoneCommand.getZoneOfPoint(pos) + + local lastLoad = cargo.squad.loadedAt + if lastLoad and zn and zn.side == cargo.object:getCoalition() and zn.name==lastLoad.name then + if self.registeredSquadGroups[cargo.squad.type] then + local cost = self.registeredSquadGroups[cargo.squad.type].cost + zn:addResource(cost) + zn:refreshText() + if cargo.unit and cargo.unit:isExist() then + local squadName = PlayerLogistics.getInfantryName(cargo.squad.type) + trigger.action.outTextForUnit(cargo.unit:getID(), squadName..' unloaded', 10) + end + end + else + local error = DependencyManager.get("SquadTracker"):spawnInfantry(self.registeredSquadGroups[cargo.squad.type], pos) + if not error then + env.info('PlayerLogistics - Hercules - '..cargo.squad.type..' deployed') + + local squadName = PlayerLogistics.getInfantryName(cargo.squad.type) + + if cargo.unit and cargo.unit:isExist() and cargo.unit.getPlayerName then + trigger.action.outTextForUnit(cargo.unit:getID(), squadName..' deployed', 10) + local player = cargo.unit:getPlayerName() + local xp = RewardDefinitions.actions.squadDeploy * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player) + + DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp) + + if zn then + DependencyManager.get("MissionTracker"):tallyUnloadSquad(player, zn.name, cargo.squad.type) + else + DependencyManager.get("MissionTracker"):tallyUnloadSquad(player, '', cargo.squad.type) + end + trigger.action.outTextForUnit(cargo.unit:getID(), '+'..math.floor(xp)..' XP', 10) + end + end + end + else + env.info('PlayerLogistics - Hercules - '..cargo.squad.type..' dropped on invalid surface '..tostring(surface)) + local cpos = cargo.object:getPoint() + env.info('PlayerLogistics - Hercules - cargo spot X:'..cpos.x..' Y:'..cpos.y..' Z:'..cpos.z) + env.info('PlayerLogistics - Hercules - surface spot X:'..pos.x..' Y:'..pos.y) + local squadName = PlayerLogistics.getInfantryName(cargo.squad.type) + trigger.action.outTextForUnit(cargo.unit:getID(), 'Cargo drop of '..cargo.unit:getPlayerName()..' with '..squadName..' crashed', 10) + end + end + + cargo.object:destroy() + end + end + + function PlayerLogistics:hercPrepareDrop(params) + local groupname = params.group + local type = params.type + local gr = Group.getByName(groupname) + if gr then + local un = gr:getUnit(1) + + if type == 'supplies' then + local cargo = self.carriedCargo[gr:getID()] + if cargo and cargo > 0 then + self.hercPreparedDrops[gr:getID()] = type + trigger.action.outTextForUnit(un:getID(), 'Supply drop prepared', 10) + else + trigger.action.outTextForUnit(un:getID(), 'No supplies onboard the aircraft', 10) + end + else + local exists = false + if self.carriedInfantry[gr:getID()] then + for i,v in ipairs(self.carriedInfantry[gr:getID()]) do + if v.type == type then + exists = true + break + end + end + end + + if exists then + self.hercPreparedDrops[gr:getID()] = type + local squadName = PlayerLogistics.getInfantryName(type) + trigger.action.outTextForUnit(un:getID(), squadName..' drop prepared', 10) + else + local squadName = PlayerLogistics.getInfantryName(type) + trigger.action.outTextForUnit(un:getID(), 'No '..squadName..' onboard the aircraft', 10) + end + end + end + end + + function PlayerLogistics:awardSupplyXP(lastLoad, zone, unit, amount) + if lastLoad and zone.name~=lastLoad.name and not zone.isCarrier and not lastLoad.isCarrier then + if unit and unit.isExist and unit:isExist() and unit.getPlayerName then + local player = unit:getPlayerName() + local xp = amount*RewardDefinitions.actions.supplyRatio + + local totalboost = 0 + local dist = mist.utils.get2DDist(lastLoad.zone.point, zone.zone.point) + if dist > 15000 then + local extradist = math.max(dist - 15000, 85000) + local kmboost = extradist/85000 + local actualboost = xp * kmboost * 1 + totalboost = totalboost + actualboost + end + + local both = true + if zone:criticalOnSupplies() then + local actualboost = xp * RewardDefinitions.actions.supplyBoost + totalboost = totalboost + actualboost + else + both = false + end + + if zone.distToFront == 0 then + local actualboost = xp * RewardDefinitions.actions.supplyBoost + totalboost = totalboost + actualboost + else + both = false + end + + if both then + local actualboost = xp * 1 + totalboost = totalboost + actualboost + end + + xp = xp + totalboost + + if lastLoad.distToFront >= zone.distToFront then + xp = xp * 0.25 + end + + xp = xp * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player) + + DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp) + DependencyManager.get("MissionTracker"):tallySupplies(player, amount, zone.name) + trigger.action.outTextForUnit(unit:getID(), '+'..math.floor(xp)..' XP', 10) + end + end + end + + function PlayerLogistics.markWithSmoke(zonename) + local zone = CustomZone:getByName(zonename) + local p = Utils.getPointOnSurface(zone.point) + trigger.action.smoke(p, 0) + end + + function PlayerLogistics.getWeight(supplies) + return math.floor(supplies) + end + + function PlayerLogistics:getCarriedPersonWeight(groupname) + local gr = Group.getByName(groupname) + local un = gr:getUnit(1) + if un then + if not PlayerLogistics.allowedTypes[un:getDesc().typeName] then return 0 end + + local max = PlayerLogistics.allowedTypes[un:getDesc().typeName].personCapacity + + local pilotWeight = 0 + local squadWeight = 0 + if not self.carriedPilots[gr:getID()] then self.carriedPilots[gr:getID()] = {} end + local pilots = self.carriedPilots[gr:getID()] + if pilots then + pilotWeight = 100 * #pilots + end + + if not self.carriedInfantry[gr:getID()] then self.carriedInfantry[gr:getID()] = {} end + local squads = self.carriedInfantry[gr:getID()] + if squads then + for _,squad in ipairs(squads) do + squadWeight = squadWeight + squad.weight + end + end + + return pilotWeight + squadWeight + end + end + + function PlayerLogistics:getOccupiedPersonCapacity(groupname) + local gr = Group.getByName(groupname) + local un = gr:getUnit(1) + if un then + if not PlayerLogistics.allowedTypes[un:getDesc().typeName] then return 0 end + if self.carriedCargo[gr:getID()] and self.carriedCargo[gr:getID()] > 0 then return 0 end + + local max = PlayerLogistics.allowedTypes[un:getDesc().typeName].personCapacity + + local pilotCount = 0 + local squadCount = 0 + if not self.carriedPilots[gr:getID()] then self.carriedPilots[gr:getID()] = {} end + local pilots = self.carriedPilots[gr:getID()] + if pilots then + pilotCount = #pilots + end + + if not self.carriedInfantry[gr:getID()] then self.carriedInfantry[gr:getID()] = {} end + local squads = self.carriedInfantry[gr:getID()] + if squads then + for _,squad in ipairs(squads) do + squadCount = squadCount + squad.size + end + end + + local total = pilotCount + squadCount + + return total + end + end + + function PlayerLogistics:getRemainingPersonCapacity(groupname) + local gr = Group.getByName(groupname) + local un = gr:getUnit(1) + if un then + if not PlayerLogistics.allowedTypes[un:getDesc().typeName] then return 0 end + if self.carriedCargo[gr:getID()] and self.carriedCargo[gr:getID()] > 0 then return 0 end + + local max = PlayerLogistics.allowedTypes[un:getDesc().typeName].personCapacity + + local total = self:getOccupiedPersonCapacity(groupname) + + return max - total + end + end + + function PlayerLogistics:canFitCargo(groupname) + local gr = Group.getByName(groupname) + local un = gr:getUnit(1) + if un then + if not PlayerLogistics.allowedTypes[un:getDesc().typeName] then return false end + return self:getOccupiedPersonCapacity(groupname) == 0 + end + end + + function PlayerLogistics:canFitPersonnel(groupname, toFit) + local gr = Group.getByName(groupname) + local un = gr:getUnit(1) + if un then + if not PlayerLogistics.allowedTypes[un:getDesc().typeName] then return false end + + return self:getRemainingPersonCapacity(groupname) >= toFit + end + end + + function PlayerLogistics:showPilot(groupname) + local gr = Group.getByName(groupname) + if gr then + local un = gr:getUnit(1) + if un then + local data = DependencyManager.get("CSARTracker"):getClosestPilot(un:getPoint()) + + if not data then + trigger.action.outTextForUnit(un:getID(), 'No pilots in need of extraction', 10) + return + end + + local pos = data.pilot:getUnit(1):getPoint() + local brg = math.floor(Utils.getBearing(un:getPoint(), data.pilot:getUnit(1):getPoint())) + local dist = data.dist + local dstft = math.floor(dist/0.3048) + + local msg = data.name..' requesting extraction' + msg = msg..'\n\n Distance: ' + if dist>1000 then + local dstkm = string.format('%.2f',dist/1000) + local dstnm = string.format('%.2f',dist/1852) + + msg = msg..dstkm..'km | '..dstnm..'nm' + else + local dstft = math.floor(dist/0.3048) + msg = msg..math.floor(dist)..'m | '..dstft..'ft' + end + + msg = msg..'\n Bearing: '..brg + + trigger.action.outTextForUnit(un:getID(), msg, 10) + end + end + end + + function PlayerLogistics:smokePilot(groupname) + local gr = Group.getByName(groupname) + if gr then + local un = gr:getUnit(1) + if un then + local data = DependencyManager.get("CSARTracker"):getClosestPilot(un:getPoint()) + + if not data or data.dist >= 5000 then + trigger.action.outTextForUnit(un:getID(), 'No pilots nearby', 10) + return + end + + DependencyManager.get("CSARTracker"):markPilot(data) + trigger.action.outTextForUnit(un:getID(), 'Location of '..data.name..' marked with green smoke.', 10) + end + end + end + + function PlayerLogistics:flarePilot(groupname) + local gr = Group.getByName(groupname) + if gr then + local un = gr:getUnit(1) + if un then + local data = DependencyManager.get("CSARTracker"):getClosestPilot(un:getPoint()) + + if not data or data.dist >= 5000 then + trigger.action.outTextForUnit(un:getID(), 'No pilots nearby', 10) + return + end + + DependencyManager.get("CSARTracker"):flarePilot(data) + trigger.action.outTextForUnit(un:getID(), data.name..' has deployed a green flare', 10) + end + end + end + + function PlayerLogistics:unloadPilots(groupname) + local gr = Group.getByName(groupname) + if gr then + local un = gr:getUnit(1) + if un then + local pilots = self.carriedPilots[gr:getID()] + if not pilots or #pilots==0 then + trigger.action.outTextForUnit(un:getID(), 'No pilots onboard', 10) + return + end + + if Utils.isInAir(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not unload pilot while in air', 10) + return + end + + if not self:isCargoDoorOpen(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not unload pilot while cargo door closed', 10) + return + end + + local zn = ZoneCommand.getZoneOfUnit(un:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(un:getName()) + end + + if not zn then + trigger.action.outTextForUnit(un:getID(), 'Can only unload extracted pilots while within a friendly zone', 10) + return + end + + if zn.side ~= 0 and zn.side ~= un:getCoalition()then + trigger.action.outTextForUnit(un:getID(), 'Can only unload extracted pilots while within a friendly zone', 10) + return + end + + zn:addResource(200*#pilots) + zn:refreshText() + + if un.getPlayerName then + local player = un:getPlayerName() + + local xp = #pilots*RewardDefinitions.actions.pilotExtract + + xp = xp * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player) + + DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp) + DependencyManager.get("MissionTracker"):tallyUnloadPilot(player, zn.name) + trigger.action.outTextForUnit(un:getID(), '+'..math.floor(xp)..' XP', 10) + end + + self.carriedPilots[gr:getID()] = {} + trigger.action.setUnitInternalCargo(un:getName(), 0) + trigger.action.outTextForUnit(un:getID(), 'Pilots unloaded', 10) + end + end + end + + function PlayerLogistics:extractPilot(groupname) + local gr = Group.getByName(groupname) + if gr then + local un = gr:getUnit(1) + if un then + if not self:canFitPersonnel(groupname, 1) then + trigger.action.outTextForUnit(un:getID(), 'Not enough free space onboard. (Need 1)', 10) + return + end + + timer.scheduleFunction(function(param,time) + local self = param.context + local un = param.unit + if not un then return end + if not un:isExist() then return end + local gr = un:getGroup() + + local data = DependencyManager.get("CSARTracker"):getClosestPilot(un:getPoint()) + + if not data or data.dist > 500 then + trigger.action.outTextForUnit(un:getID(), 'There is no pilot nearby that needs extraction', 10) + return + else + if not self:isCargoDoorOpen(un) then + trigger.action.outTextForUnit(un:getID(), 'Cargo door closed', 1) + elseif Utils.getAGL(un) > 70 then + trigger.action.outTextForUnit(un:getID(), 'Altitude too high (< 70 m). Current: '..string.format('%.2f',Utils.getAGL(un))..' m', 1) + elseif mist.vec.mag(un:getVelocity())>5 then + trigger.action.outTextForUnit(un:getID(), 'Moving too fast (< 5 m/s). Current: '..string.format('%.2f',mist.vec.mag(un:getVelocity()))..' m/s', 1) + else + if data.dist > 100 then + trigger.action.outTextForUnit(un:getID(), 'Too far (< 100m). Current: '..string.format('%.2f',data.dist)..' m', 1) + else + if not self.carriedPilots[gr:getID()] then self.carriedPilots[gr:getID()] = {} end + table.insert(self.carriedPilots[gr:getID()], data.name) + local player = un:getPlayerName() + DependencyManager.get("MissionTracker"):tallyLoadPilot(player, data) + DependencyManager.get("CSARTracker"):removePilot(data.name) + local weight = self:getCarriedPersonWeight(gr:getName()) + trigger.action.setUnitInternalCargo(un:getName(), weight) + trigger.action.outTextForUnit(un:getID(), data.name..' onboard. ('..weight..' kg)', 10) + return + end + end + end + + param.trys = param.trys - 1 + if param.trys > 0 then + return time+1 + end + end, {context = self, unit = un, trys = 60}, timer.getTime()+0.1) + end + end + end + + function PlayerLogistics:extractSquad(groupname) + local gr = Group.getByName(groupname) + if gr then + local un = gr:getUnit(1) + if un then + + if Utils.isInAir(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not load infantry while in air', 10) + return + end + + if not self:isCargoDoorOpen(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not load infantry while cargo door closed', 10) + return + end + + local squad, distance = DependencyManager.get("SquadTracker"):getClosestExtractableSquad(un:getPoint(), un:getCoalition()) + if squad and distance < 50 then + local squadgr = Group.getByName(squad.name) + if squadgr and squadgr:isExist() then + local sqsize = squadgr:getSize() + if not self:canFitPersonnel(groupname, sqsize) then + trigger.action.outTextForUnit(un:getID(), 'Not enough free space onboard. (Need '..sqsize..')', 10) + return + end + + if not self.carriedInfantry[gr:getID()] then self.carriedInfantry[gr:getID()] = {} end + table.insert(self.carriedInfantry[gr:getID()],{type = PlayerLogistics.infantryTypes.extractable, size = sqsize, weight = sqsize * 100}) + + local weight = self:getCarriedPersonWeight(gr:getName()) + + trigger.action.setUnitInternalCargo(un:getName(), weight) + + local loadedInfName = PlayerLogistics.getInfantryName(PlayerLogistics.infantryTypes.extractable) + trigger.action.outTextForUnit(un:getID(), loadedInfName..' onboard. ('..weight..' kg)', 10) + + local player = un:getPlayerName() + DependencyManager.get("MissionTracker"):tallyLoadSquad(player, squad) + DependencyManager.get("SquadTracker"):removeSquad(squad.name) + + squadgr:destroy() + end + else + trigger.action.outTextForUnit(un:getID(), 'There is no infantry nearby that is ready to be extracted.', 10) + end + end + end + end + + function PlayerLogistics:loadInfantry(params) + if not ZoneCommand then return end + + local gr = Group.getByName(params.group) + local squadType = params.type + local squadName = PlayerLogistics.getInfantryName(squadType) + + local squadCost = 0 + local squadSize = 999999 + local squadWeight = 0 + if self.registeredSquadGroups[squadType] then + squadCost = self.registeredSquadGroups[squadType].cost + squadSize = self.registeredSquadGroups[squadType].size + squadWeight = self.registeredSquadGroups[squadType].weight + end + + if gr then + local un = gr:getUnit(1) + if un then + if Utils.isInAir(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not load infantry while in air', 10) + return + end + + local zn = ZoneCommand.getZoneOfUnit(un:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(un:getName()) + end + + if not zn then + trigger.action.outTextForUnit(un:getID(), 'Can only load infantry while within a friendly zone', 10) + return + end + + if zn.side ~= un:getCoalition() then + trigger.action.outTextForUnit(un:getID(), 'Can only load infantry while within a friendly zone', 10) + return + end + + if not self:isCargoDoorOpen(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not load infantry while cargo door closed', 10) + return + end + + if zn:criticalOnSupplies() then + trigger.action.outTextForUnit(un:getID(), 'Can not load infantry, zone is low on resources', 10) + return + end + + if zn.resource < zn.spendTreshold + squadCost then + trigger.action.outTextForUnit(un:getID(), 'Can not afford to load '..squadName..' (Cost: '..squadCost..'). Resources would fall to a critical level.', 10) + return + end + + if not self:canFitPersonnel(params.group, squadSize) then + trigger.action.outTextForUnit(un:getID(), 'Not enough free space on board. (Need '..squadSize..')', 10) + return + end + + zn:removeResource(squadCost) + zn:refreshText() + if not self.carriedInfantry[gr:getID()] then self.carriedInfantry[gr:getID()] = {} end + table.insert(self.carriedInfantry[gr:getID()],{ type = squadType, size = squadSize, weight = squadWeight, loadedAt = zn }) + self.lastLoaded[gr:getID()] = zn + + local weight = self:getCarriedPersonWeight(gr:getName()) + trigger.action.setUnitInternalCargo(un:getName(), weight) + + local loadedInfName = PlayerLogistics.getInfantryName(squadType) + trigger.action.outTextForUnit(un:getID(), loadedInfName..' onboard. ('..weight..' kg)', 10) + end + end + end + + function PlayerLogistics:unloadInfantry(params) + if not ZoneCommand then return end + local groupname = params.group + local sqtype = params.type + + local gr = Group.getByName(groupname) + if gr then + local un = gr:getUnit(1) + if un then + if Utils.isInAir(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not unload infantry while in air', 10) + return + end + + if not self:isCargoDoorOpen(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not unload infantry while cargo door closed', 10) + return + end + + local carriedSquads = self.carriedInfantry[gr:getID()] + if not carriedSquads or #carriedSquads == 0 then + trigger.action.outTextForUnit(un:getID(), 'No infantry onboard', 10) + return + end + + local toUnload = carriedSquads + local remaining = {} + if sqtype then + toUnload = {} + local sqToUnload = nil + for _,sq in ipairs(carriedSquads) do + if sq.type == sqtype and not sqToUnload then + sqToUnload = sq + else + table.insert(remaining, sq) + end + end + + if sqToUnload then toUnload = { sqToUnload } end + end + + if #toUnload == 0 then + if sqtype then + local squadName = PlayerLogistics.getInfantryName(sqtype) + trigger.action.outTextForUnit(un:getID(), 'No '..squadName..' onboard.', 10) + else + trigger.action.outTextForUnit(un:getID(), 'No infantry onboard.', 10) + end + + return + end + + local zn = ZoneCommand.getZoneOfUnit(un:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(un:getName()) + end + + for _, sq in ipairs(toUnload) do + local squadName = PlayerLogistics.getInfantryName(sq.type) + local lastLoad = sq.loadedAt + if lastLoad and zn and zn.side == un:getCoalition() and zn.name==lastLoad.name then + if self.registeredSquadGroups[sq.type] then + local cost = self.registeredSquadGroups[sq.type].cost + zn:addResource(cost) + zn:refreshText() + trigger.action.outTextForUnit(un:getID(), squadName..' unloaded', 10) + end + else + if sq.type == PlayerLogistics.infantryTypes.extractable then + if not zn then + trigger.action.outTextForUnit(un:getID(), 'Can only unload extracted infantry while within a friendly zone', 10) + table.insert(remaining, sq) + elseif zn.side ~= un:getCoalition() then + trigger.action.outTextForUnit(un:getID(), 'Can only unload extracted infantry while within a friendly zone', 10) + table.insert(remaining, sq) + else + trigger.action.outTextForUnit(un:getID(), 'Infantry recovered', 10) + zn:addResource(200) + zn:refreshText() + + if un.getPlayerName then + local player = un:getPlayerName() + local xp = RewardDefinitions.actions.squadExtract * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player) + + DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp) + DependencyManager.get("MissionTracker"):tallyUnloadSquad(player, zn.name, sq.type) + trigger.action.outTextForUnit(un:getID(), '+'..math.floor(xp)..' XP', 10) + end + end + elseif self.registeredSquadGroups[sq.type] then + local pos = Utils.getPointOnSurface(un:getPoint()) + + local error = DependencyManager.get("SquadTracker"):spawnInfantry(self.registeredSquadGroups[sq.type], pos) + + if not error then + trigger.action.outTextForUnit(un:getID(), squadName..' deployed', 10) + + if un.getPlayerName then + local player = un:getPlayerName() + local xp = RewardDefinitions.actions.squadDeploy * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player) + + DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp) + + if zn then + DependencyManager.get("MissionTracker"):tallyUnloadSquad(player, zn.name, sq.type) + else + DependencyManager.get("MissionTracker"):tallyUnloadSquad(player, '', sq.type) + end + trigger.action.outTextForUnit(un:getID(), '+'..math.floor(xp)..' XP', 10) + end + else + trigger.action.outTextForUnit(un:getID(), 'Failed to deploy squad, no suitable location nearby', 10) + table.insert(remaining, sq) + end + else + trigger.action.outText("ERROR: SQUAD TYPE NOT REGISTERED", 60) + end + end + end + + self.carriedInfantry[gr:getID()] = remaining + local weight = self:getCarriedPersonWeight(groupname) + trigger.action.setUnitInternalCargo(un:getName(), weight) + end + end + end + + function PlayerLogistics:unloadAll(groupname) + local gr = Group.getByName(groupname) + if gr then + local un = gr:getUnit(1) + if un then + local cargo = self.carriedCargo[gr:getID()] + local squad = self.carriedInfantry[gr:getID()] + local pilot = self.carriedPilots[gr:getID()] + + if cargo and cargo>0 then + self:unloadSupplies({group=groupname, amount=9999999}) + end + + if squad and #squad>0 then + self:unloadInfantry({group=groupname}) + end + + if pilot and #pilot>0 then + self:unloadPilots(groupname) + end + end + end + end + + function PlayerLogistics:cargoStatus(groupName) + local gr = Group.getByName(groupName) + if gr then + local un = gr:getUnit(1) + if un then + local onboard = self.carriedCargo[gr:getID()] + if onboard and onboard > 0 then + local weight = self.getWeight(onboard) + trigger.action.outTextForUnit(un:getID(), onboard..' supplies onboard. ('..weight..' kg)', 10) + else + local msg = '' + local squads = self.carriedInfantry[gr:getID()] + if squads and #squads>0 then + msg = msg..'Squads:\n' + + for _,squad in ipairs(squads) do + local infName = PlayerLogistics.getInfantryName(squad.type) + msg = msg..' \n'..infName..' (Size: '..squad.size..')' + end + end + + local pilots = self.carriedPilots[gr:getID()] + if pilots and #pilots>0 then + msg = msg.."\n\nPilots:\n" + for i,v in ipairs(pilots) do + msg = msg..'\n '..v + end + + end + + local max = PlayerLogistics.allowedTypes[un:getDesc().typeName].personCapacity + local occupied = self:getOccupiedPersonCapacity(groupName) + + msg = msg..'\n\nCapacity: '..occupied..'/'..max + + msg = msg..'\n('..self:getCarriedPersonWeight(groupName)..' kg)' + + if un:getDesc().typeName == 'Hercules' then + local preped = self.hercPreparedDrops[gr:getID()] + if preped then + if preped == 'supplies' then + msg = msg..'\nSupplies prepared for next drop.' + else + local squadName = PlayerLogistics.getInfantryName(preped) + msg = msg..'\n'..squadName..' prepared for next drop.' + end + end + end + + trigger.action.outTextForUnit(un:getID(), msg, 10) + end + end + end + end + + function PlayerLogistics:isCargoDoorOpen(unit) + if unit then + local tp = unit:getDesc().typeName + if tp == "Mi-8MT" then + if unit:getDrawArgumentValue(86) == 1 then return true end + if unit:getDrawArgumentValue(38) > 0.85 then return true end + elseif tp == "UH-1H" then + if unit:getDrawArgumentValue(43) == 1 then return true end + if unit:getDrawArgumentValue(44) == 1 then return true end + elseif tp == "Mi-24P" then + if unit:getDrawArgumentValue(38) == 1 then return true end + if unit:getDrawArgumentValue(86) == 1 then return true end + elseif tp == "Hercules" then + if unit:getDrawArgumentValue(1215) == 1 and unit:getDrawArgumentValue(1216) == 1 then return true end + elseif tp == "UH-60L" then + if unit:getDrawArgumentValue(401) == 1 then return true end + if unit:getDrawArgumentValue(402) == 1 then return true end + elseif tp == "SA342Mistral" then + if unit:getDrawArgumentValue(34) == 1 then return true end + if unit:getDrawArgumentValue(38) == 1 then return true end + elseif tp == "SA342L" then + if unit:getDrawArgumentValue(34) == 1 then return true end + if unit:getDrawArgumentValue(38) == 1 then return true end + elseif tp == "SA342M" then + if unit:getDrawArgumentValue(34) == 1 then return true end + if unit:getDrawArgumentValue(38) == 1 then return true end + elseif tp == "SA342Minigun" then + if unit:getDrawArgumentValue(34) == 1 then return true end + if unit:getDrawArgumentValue(38) == 1 then return true end + else + return true + end + end + end + + function PlayerLogistics:loadSupplies(params) + if not ZoneCommand then return end + + local groupName = params.group + local amount = params.amount + + local gr = Group.getByName(groupName) + if gr then + local un = gr:getUnit(1) + if un then + if not self:canFitCargo(groupName) then + trigger.action.outTextForUnit(un:getID(), 'Can not load cargo. Personnel onboard.', 10) + return + end + + if Utils.isInAir(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not load supplies while in air', 10) + return + end + + local zn = ZoneCommand.getZoneOfUnit(un:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(un:getName()) + end + + if not zn then + trigger.action.outTextForUnit(un:getID(), 'Can only load supplies while within a friendly zone', 10) + return + end + + if zn.side ~= un:getCoalition() then + trigger.action.outTextForUnit(un:getID(), 'Can only load supplies while within a friendly zone', 10) + return + end + + if not self:isCargoDoorOpen(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not load supplies while cargo door closed', 10) + return + end + + if zn:criticalOnSupplies() then + trigger.action.outTextForUnit(un:getID(), 'Can not load supplies, zone is low on resources', 10) + return + end + + if zn.resource < zn.spendTreshold + amount then + trigger.action.outTextForUnit(un:getID(), 'Can not load supplies if resources would fall to a critical level.', 10) + return + end + + local carried = self.carriedCargo[gr:getID()] or 0 + if amount > zn.resource then + amount = zn.resource + end + + zn:removeResource(amount) + zn:refreshText() + self.carriedCargo[gr:getID()] = carried + amount + self.lastLoaded[gr:getID()] = zn + local onboard = self.carriedCargo[gr:getID()] + local weight = self.getWeight(onboard) + + if un:getDesc().typeName == "Hercules" then + local loadedInCrates = 0 + local ammo = un:getAmmo() + if ammo then + for _,load in ipairs(ammo) do + if load.desc.typeName == 'weapons.bombs.Generic Crate [20000lb]' then + loadedInCrates = 9000 * load.count + end + end + end + + local internal = 0 + if weight > loadedInCrates then + internal = weight - loadedInCrates + end + + trigger.action.setUnitInternalCargo(un:getName(), internal) + else + trigger.action.setUnitInternalCargo(un:getName(), weight) + end + + trigger.action.outTextForUnit(un:getID(), amount..' supplies loaded', 10) + trigger.action.outTextForUnit(un:getID(), onboard..' supplies onboard. ('..weight..' kg)', 10) + end + end + end + + function PlayerLogistics:unloadSupplies(params) + if not ZoneCommand then return end + + local groupName = params.group + local amount = params.amount + + local gr = Group.getByName(groupName) + if gr then + local un = gr:getUnit(1) + if un then + if Utils.isInAir(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not unload supplies while in air', 10) + return + end + + local zn = ZoneCommand.getZoneOfUnit(un:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(un:getName()) + end + + if not zn then + trigger.action.outTextForUnit(un:getID(), 'Can only unload supplies while within a friendly zone', 10) + return + end + + if zn.side ~= 0 and zn.side ~= un:getCoalition()then + trigger.action.outTextForUnit(un:getID(), 'Can only unload supplies while within a friendly zone', 10) + return + end + + if not self:isCargoDoorOpen(un) then + trigger.action.outTextForUnit(un:getID(), 'Can not unload supplies while cargo door closed', 10) + return + end + + if not self.carriedCargo[gr:getID()] or self.carriedCargo[gr:getID()] == 0 then + trigger.action.outTextForUnit(un:getID(), 'No supplies loaded', 10) + return + end + + local carried = self.carriedCargo[gr:getID()] + if amount > carried then + amount = carried + end + + self.carriedCargo[gr:getID()] = carried-amount + zn:addResource(amount) + + local lastLoad = self.lastLoaded[gr:getID()] + self:awardSupplyXP(lastLoad, zn, un, amount) + + zn:refreshText() + local onboard = self.carriedCargo[gr:getID()] + local weight = self.getWeight(onboard) + + if un:getDesc().typeName == "Hercules" then + local loadedInCrates = 0 + local ammo = un:getAmmo() + for _,load in ipairs(ammo) do + if load.desc.typeName == 'weapons.bombs.Generic Crate [20000lb]' then + loadedInCrates = 9000 * load.count + end + end + + local internal = 0 + if weight > loadedInCrates then + internal = weight - loadedInCrates + end + + trigger.action.setUnitInternalCargo(un:getName(), internal) + else + trigger.action.setUnitInternalCargo(un:getName(), weight) + end + + trigger.action.outTextForUnit(un:getID(), amount..' supplies unloaded', 10) + trigger.action.outTextForUnit(un:getID(), onboard..' supplies remaining onboard. ('..weight..' kg)', 10) + end + end + end +end + +-----------------[[ END OF PlayerLogistics.lua ]]----------------- + + + +-----------------[[ GroupMonitor.lua ]]----------------- + +GroupMonitor = {} +do + GroupMonitor.blockedDespawnTime = 10*60 --used to despawn aircraft that are stuck taxiing for some reason + GroupMonitor.landedDespawnTime = 10 + GroupMonitor.atDestinationDespawnTime = 2*60 + GroupMonitor.recoveryReduction = 0.8 -- reduce recovered resource from landed missions by this amount to account for maintenance + + GroupMonitor.siegeExplosiveTime = 5*60 -- how long until random upgrade is detonated in zone + GroupMonitor.siegeExplosiveStrength = 1000 -- detonation strength + + GroupMonitor.timeBeforeSquadDeploy = 10*60 + GroupMonitor.squadChance = 0.001 + GroupMonitor.ambushChance = 0.7 + + GroupMonitor.aiSquads = { + ambush = { + [1] = { + name='ambush-squad-red', + type=PlayerLogistics.infantryTypes.ambush, + weight = 900, + cost= 300, + jobtime= 60*60*2, + extracttime= 0, + size = 5, + side = 1, + isAISpawned = true + }, + [2] = { + name='ambush-squad', + type=PlayerLogistics.infantryTypes.ambush, + weight = 900, + cost= 300, + jobtime= 60*60, + extracttime= 60*30, + size = 5, + side = 2, + isAISpawned = true + }, + }, + manpads = { + [1] = { + name='manpads-squad-red', + type=PlayerLogistics.infantryTypes.manpads, + weight = 900, + cost= 500, + jobtime= 60*60*2, + extracttime= 0, + size = 5, + side= 1, + isAISpawned = true + }, + [2] = { + name='manpads-squad', + type=PlayerLogistics.infantryTypes.manpads, + weight = 900, + cost= 500, + jobtime= 60*60, + extracttime= 60*30, + size = 5, + side= 2, + isAISpawned = true + } + } + } + + function GroupMonitor:new() + local obj = {} + obj.groups = {} + setmetatable(obj, self) + self.__index = self + + obj:start() + + DependencyManager.register("GroupMonitor", obj) + return obj + end + + function GroupMonitor.isAirAttack(misType) + if misType == ZoneCommand.missionTypes.cas then return true end + if misType == ZoneCommand.missionTypes.cas_helo then return true end + if misType == ZoneCommand.missionTypes.strike then return true end + if misType == ZoneCommand.missionTypes.patrol then return true end + if misType == ZoneCommand.missionTypes.sead then return true end + if misType == ZoneCommand.missionTypes.bai then return true end + end + + function GroupMonitor.hasWeapons(group) + for _,un in ipairs(group:getUnits()) do + local wps = un:getAmmo() + if wps then + for _,w in ipairs(wps) do + if w.desc.category ~= 0 and w.count > 0 then + return true + end + end + end + end + end + + function GroupMonitor:sendHome(trackedGroup) + if trackedGroup.home == nil then + env.info("GroupMonitor - sendHome "..trackedGroup.name..' does not have home set') + return + end + + if trackedGroup.returning then return end + + + local gr = Group.getByName(trackedGroup.name) + if gr then + if trackedGroup.product.missionType == ZoneCommand.missionTypes.cas_helo then + local hsp = trigger.misc.getZone(trackedGroup.home.name..'-hsp') + if not hsp then + hsp = trigger.misc.getZone(trackedGroup.home.name) + end + + local alt = DependencyManager.get("ConnectionManager"):getHeliAlt(trackedGroup.target.name, trackedGroup.home.name) + TaskExtensions.landAtPointFromAir(gr, {x=hsp.point.x, y=hsp.point.z}, alt) + else + local homeZn = trigger.misc.getZone(trackedGroup.home.name) + TaskExtensions.landAtAirfield(gr, {x=homeZn.point.x, y=homeZn.point.z}) + end + + local cnt = gr:getController() + cnt:setOption(0,4) -- force ai hold fire + cnt:setOption(1, 4) -- force reaction on threat to allow abort + + trackedGroup.returning = true + env.info('GroupMonitor - sendHome ['..trackedGroup.name..'] returning home') + end + end + + function GroupMonitor:registerGroup(product, target, home, savedData) + self.groups[product.name] = {name = product.name, lastStateTime = timer.getAbsTime(), product = product, target = target, home = home, stuck_marker = 0} + + if savedData and savedData.state ~= 'uninitialized' then + env.info('GroupMonitor - registerGroup ['..product.name..'] restored state '..savedData.state..' dur:'..savedData.lastStateDuration) + self.groups[product.name].state = savedData.state + self.groups[product.name].lastStateTime = timer.getAbsTime() - savedData.lastStateDuration + self.groups[product.name].spawnedSquad = savedData.spawnedSquad + end + end + + function GroupMonitor:start() + timer.scheduleFunction(function(param, time) + local self = param.context + + for i,v in pairs(self.groups) do + local isDead = false + if v.product.missionType == 'supply_convoy' or v.product.missionType == 'assault' then + isDead = self:processSurface(v) + if isDead then + MissionTargetRegistry.removeBaiTarget(v) --safety measure in case group is dead + end + else + isDead = self:processAir(v) + end + + if isDead then + self.groups[i] = nil + end + end + + return time+10 + end, {context = self}, timer.getTime()+1) + end + + function GroupMonitor:getGroup(name) + return self.groups[name] + end + + function GroupMonitor:processSurface(group) -- states: [started, enroute, atdestination, siege] + local gr = Group.getByName(group.name) + if not gr then return true end + if gr:getSize()==0 then + gr:destroy() + return true + end + + if not group.state then + group.state = 'started' + group.lastStateTime = timer.getAbsTime() + env.info('GroupMonitor: processSurface ['..group.name..'] starting') + end + + if group.state =='started' then + if gr then + local firstUnit = gr:getUnit(1):getName() + local z = ZoneCommand.getZoneOfUnit(firstUnit) + if not z then + z = CarrierCommand.getCarrierOfUnit(firstUnit) + end + + if not z then + env.info('GroupMonitor: processSurface ['..group.name..'] is enroute') + group.state = 'enroute' + group.lastStateTime = timer.getAbsTime() + MissionTargetRegistry.addBaiTarget(group) + elseif timer.getAbsTime() - group.lastStateTime > GroupMonitor.blockedDespawnTime then + env.info('GroupMonitor: processSurface ['..group.name..'] despawned due to blockage') + gr:destroy() + local todeliver = math.floor(group.product.cost) + z:addResource(todeliver) + return true + end + end + elseif group.state =='enroute' then + if gr then + local firstUnit = gr:getUnit(1):getName() + local z = ZoneCommand.getZoneOfUnit(firstUnit) + if not z then + z = CarrierCommand.getCarrierOfUnit(firstUnit) + end + + if z and (z.name==group.target.name or z.name==group.home.name) then + MissionTargetRegistry.removeBaiTarget(group) + + if group.product.missionType == 'supply_convoy' then + env.info('GroupMonitor: processSurface ['..group.name..'] has arrived at destination') + group.state = 'atdestination' + group.lastStateTime = timer.getAbsTime() + z:capture(gr:getCoalition()) + local percentSurvived = gr:getSize()/gr:getInitialSize() + local todeliver = math.floor(group.product.cost * percentSurvived) + z:addResource(todeliver) + env.info('GroupMonitor: processSurface ['..group.name..'] has supplied ['..z.name..'] with ['..todeliver..']') + elseif group.product.missionType == 'assault' then + if z.side == gr:getCoalition() then + env.info('GroupMonitor: processSurface ['..group.name..'] has arrived at destination') + group.state = 'atdestination' + group.lastStateTime = timer.getAbsTime() + local percentSurvived = gr:getSize()/gr:getInitialSize() + local torecover = math.floor(group.product.cost * percentSurvived * GroupMonitor.recoveryReduction) + z:addResource(torecover) + env.info('GroupMonitor: processSurface ['..z.name..'] has recovered ['..torecover..'] from ['..group.name..']') + elseif z.side == 0 then + env.info('GroupMonitor: processSurface ['..group.name..'] has arrived at destination') + group.state = 'atdestination' + group.lastStateTime = timer.getAbsTime() + z:capture(gr:getCoalition()) + env.info('GroupMonitor: processSurface ['..group.name..'] has captured ['..z.name..']') + elseif z.side ~= gr:getCoalition() and z.side ~= 0 then + env.info('GroupMonitor: processSurface ['..group.name..'] starting siege') + group.state = 'siege' + group.lastStateTime = timer.getAbsTime() + end + end + else + if group.product.missionType == 'supply_convoy' then + if not group.returning and group.target and group.target.side ~= group.product.side and group.target.side ~= 0 then + local supplyPoint = trigger.misc.getZone(group.home.name..'-sp') + if not supplyPoint then + supplyPoint = trigger.misc.getZone(group.home.name) + end + + if supplyPoint then + group.returning = true + env.info('GroupMonitor: processSurface ['..group.name..'] returning home') + TaskExtensions.moveOnRoadToPoint(gr, {x=supplyPoint.point.x, y=supplyPoint.point.z}) + end + elseif GroupMonitor.isStuck(group) then + env.info('GroupMonitor: processSurface ['..group.name..'] is stuck, trying to get unstuck') + + local tgtname = group.target.name + if group.returning then + tgtname = group.home.name + end + + local supplyPoint = trigger.misc.getZone(tgtname..'-sp') + if not supplyPoint then + supplyPoint = trigger.misc.getZone(tgtname) + end + TaskExtensions.moveOnRoadToPoint(gr, {x=supplyPoint.point.x, y=supplyPoint.point.z}, true) + + group.unstuck_attempts = group.unstuck_attempts or 0 + group.unstuck_attempts = group.unstuck_attempts + 1 + + if group.unstuck_attempts >= 5 then + env.info('GroupMonitor: processSurface ['..group.name..'] is stuck, trying to get unstuck by teleport') + group.unstuck_attempts = 0 + local frUnit = gr:getUnit(1) + local pos = frUnit:getPoint() + + mist.teleportToPoint({ + groupName = group.name, + action = 'teleport', + initTasks = false, + point = {x=pos.x+math.random(-25,25), y=pos.y, z = pos.z+math.random(-25,25)} + }) + + timer.scheduleFunction(function(params, time) + local group = params.gr + local tgtname = group.target.name + if group.returning then + tgtname = group.home.name + end + local gr = Group.getByName(group.name) + local supplyPoint = trigger.misc.getZone(tgtname..'-sp') + if not supplyPoint then + supplyPoint = trigger.misc.getZone(tgtname) + end + + TaskExtensions.moveOnRoadToPoint(gr, {x=supplyPoint.point.x, y=supplyPoint.point.z}, true) + end, {gr = group}, timer.getTime()+2) + end + end + elseif group.product.missionType == 'assault' then + local frUnit = gr:getUnit(1) + if frUnit then + local skipDetection = false + if group.lastStarted and (timer.getAbsTime() - group.lastStarted) < (30) then + skipDetection = true + else + group.lastStarted = nil + end + + local shouldstop = false + if not skipDetection then + local controller = frUnit:getController() + local targets = controller:getDetectedTargets() + + if #targets > 0 then + for _,tgt in ipairs(targets) do + if tgt.visible and tgt.object then + if tgt.object.isExist and tgt.object:isExist() and tgt.object.getCoalition and tgt.object:getCoalition()~=frUnit:getCoalition() and + Object.getCategory(tgt.object) == 1 then + local dist = mist.utils.get3DDist(frUnit:getPoint(), tgt.object:getPoint()) + if dist < 700 then + if not group.isstopped then + env.info('GroupMonitor: processSurface ['..group.name..'] stopping to engage targets') + TaskExtensions.stopAndDisperse(gr) + group.isstopped = true + group.lastStopped = timer.getAbsTime() + end + shouldstop = true + break + end + end + end + end + end + end + + if group.lastStopped then + if (timer.getAbsTime() - group.lastStopped) > (3*60) then + env.info('GroupMonitor: processSurface ['..group.name..'] override stop, waited too long') + shouldstop = false + group.lastStarted = timer.getAbsTime() + end + end + + if not shouldstop and group.isstopped then + env.info('GroupMonitor: processSurface ['..group.name..'] resuming mission') + local tp = { + x = group.target.zone.point.x, + y = group.target.zone.point.z + } + + TaskExtensions.moveOnRoadToPointAndAssault(gr, tp, group.target.built) + group.isstopped = false + group.lastStopped = nil + end + + if not shouldstop and not group.isstopped then + if GroupMonitor.isStuck(group) then + env.info('GroupMonitor: processSurface ['..group.name..'] is stuck, trying to get unstuck') + local tp = { + x = group.target.zone.point.x, + y = group.target.zone.point.z + } + + TaskExtensions.moveOnRoadToPointAndAssault(gr, tp, group.target.built, true) + + group.unstuck_attempts = group.unstuck_attempts or 0 + group.unstuck_attempts = group.unstuck_attempts + 1 + + if group.unstuck_attempts >= 5 then + env.info('GroupMonitor: processSurface ['..group.name..'] is stuck, trying to get unstuck by teleport') + group.unstuck_attempts = 0 + local pos = frUnit:getPoint() + + mist.teleportToPoint({ + groupName = group.name, + action = 'teleport', + initTasks = false, + point = {x=pos.x+math.random(-25,25), y=pos.y, z = pos.z+math.random(-25,25)} + }) + + timer.scheduleFunction(function(params, time) + local group = params.group + local gr = Group.getByName(gr) + local tp = { + x = group.target.zone.point.x, + y = group.target.zone.point.z + } + + TaskExtensions.moveOnRoadToPointAndAssault(gr, tp, group.target.built, true) + end, {gr = group}, timer.getTime()+2) + end + elseif group.unstuck_attempts and group.unstuck_attempts > 0 then + group.unstuck_attempts = 0 + end + end + + local timeElapsed = timer.getAbsTime() - group.lastStateTime + if not group.spawnedSquad and timeElapsed > GroupMonitor.timeBeforeSquadDeploy then + local die = math.random() + if die < GroupMonitor.squadChance then + local pos = gr:getUnit(1):getPoint() + + local squadData = nil + if math.random() > GroupMonitor.ambushChance then + squadData = GroupMonitor.aiSquads.manpads[gr:getCoalition()] + else + squadData = GroupMonitor.aiSquads.ambush[gr:getCoalition()] + end + + DependencyManager.get("SquadTracker"):spawnInfantry(squadData, pos) + env.info('GroupMonitor: processSurface ['..group.name..'] has deployed '..squadData.type..' squad') + group.spawnedSquad = true + end + end + end + end + end + end + elseif group.state == 'atdestination' then + if timer.getAbsTime() - group.lastStateTime > GroupMonitor.atDestinationDespawnTime then + + if gr then + local firstUnit = gr:getUnit(1):getName() + local z = ZoneCommand.getZoneOfUnit(firstUnit) + if not z then + z = CarrierCommand.getCarrierOfUnit(firstUnit) + end + if z and z.side == 0 then + env.info('GroupMonitor: processSurface ['..group.name..'] is at neutral zone') + z:capture(gr:getCoalition()) + env.info('GroupMonitor: processSurface ['..group.name..'] has captured ['..z.name..']') + else + env.info('GroupMonitor: processSurface ['..group.name..'] starting siege') + group.state = 'siege' + group.lastStateTime = timer.getAbsTime() + end + + env.info('GroupMonitor: processSurface ['..group.name..'] despawned after arriving at destination') + gr:destroy() + return true + end + end + elseif group.state == 'siege' then + if group.product.missionType ~= 'assault' then + group.state = 'atdestination' + group.lastStateTime = timer.getAbsTime() + else + if timer.getAbsTime() - group.lastStateTime > GroupMonitor.siegeExplosiveTime then + if gr then + local firstUnit = gr:getUnit(1):getName() + local z = ZoneCommand.getZoneOfUnit(firstUnit) + local success = false + + if z then + for i,v in pairs(z.built) do + if v.type == 'upgrade' and v.side ~= gr:getCoalition() then + local st = StaticObject.getByName(v.name) + if not st then st = Group.getByName(v.name) end + local pos = st:getPoint() + trigger.action.explosion(pos, GroupMonitor.siegeExplosiveStrength) + group.lastStateTime = timer.getAbsTime() + success = true + env.info('GroupMonitor: processSurface ['..group.name..'] detonating structure at '..z.name) + break + end + end + end + + if not success then + env.info('GroupMonitor: processSurface ['..group.name..'] no targets to detonate, switching to atdestination') + group.state = 'atdestination' + group.lastStateTime = timer.getAbsTime() + end + end + end + end + end + end + + function GroupMonitor.isStuck(group) + local gr = Group.getByName(group.name) + if not gr then return false end + if gr:getSize() == 0 then return false end + + local un = gr:getUnit(1) + if un and un:isExist() and mist.vec.mag(un:getVelocity()) >= 0.01 and group.stuck_marker > 0 then + group.stuck_marker = 0 + group.unstuck_attempts = 0 + env.info('GroupMonitor: isStuck ['..group.name..'] is moving, reseting stuck marker velocity='..mist.vec.mag(un:getVelocity())) + end + + if un and un:isExist() and mist.vec.mag(un:getVelocity()) < 0.01 then + group.stuck_marker = group.stuck_marker + 1 + env.info('GroupMonitor: isStuck ['..group.name..'] is not moving, increasing stuck marker to '..group.stuck_marker..' velocity='..mist.vec.mag(un:getVelocity())) + + if group.stuck_marker >= 3 then + group.stuck_marker = 0 + env.info('GroupMonitor: isStuck ['..group.name..'] is stuck') + return true + end + end + + return false + end + + function GroupMonitor:processAir(group)-- states: [takeoff, inair, landed] + local gr = Group.getByName(group.name) + if not gr then return true end + if not gr:isExist() or gr:getSize()==0 then + gr:destroy() + return true + end + --[[ + if group.product.missionType == 'cas' or group.product.missionType == 'cas_helo' or group.product.missionType == 'strike' or group.product.missionType == 'sead' then + if MissionTargetRegistry.isZoneTargeted(group.target) and group.product.side == 2 and not group.returning then + env.info('GroupMonitor - mission ['..group.name..'] to ['..group.target..'] canceled due to player mission') + + GroupMonitor.sendHome(group) + end + end + ]]-- + + if not group.state then + group.state = 'takeoff' + env.info('GroupMonitor: processAir ['..group.name..'] taking off') + end + + if group.state =='takeoff' then + if timer.getAbsTime() - group.lastStateTime > GroupMonitor.blockedDespawnTime then + if gr and gr:getSize()>0 and gr:getUnit(1):isExist() then + local frUnit = gr:getUnit(1) + local cz = CarrierCommand.getCarrierOfUnit(frUnit:getName()) + if Utils.allGroupIsLanded(gr, cz ~= nil) then + env.info('GroupMonitor: processAir ['..group.name..'] is blocked, despawning') + local frUnit = gr:getUnit(1) + if frUnit then + local firstUnit = frUnit:getName() + local z = ZoneCommand.getZoneOfUnit(firstUnit) + if not z then + z = CarrierCommand.getCarrierOfUnit(firstUnit) + end + if z then + z:addResource(group.product.cost) + env.info('GroupMonitor: processAir ['..z.name..'] has recovered ['..group.product.cost..'] from ['..group.name..']') + end + end + + gr:destroy() + return true + end + end + elseif gr and Utils.someOfGroupInAir(gr) then + env.info('GroupMonitor: processAir ['..group.name..'] is in the air') + group.state = 'inair' + group.lastStateTime = timer.getAbsTime() + end + elseif group.state =='inair' then + if gr then + local unit = gr:getUnit(1) + if not unit or not unit:isExist() then return end + + local cz = CarrierCommand.getCarrierOfUnit(unit:getName()) + if Utils.allGroupIsLanded(gr, cz ~= nil) then + env.info('GroupMonitor: processAir ['..group.name..'] has landed') + group.state = 'landed' + group.lastStateTime = timer.getAbsTime() + + if unit then + local firstUnit = unit:getName() + local z = ZoneCommand.getZoneOfUnit(firstUnit) + if not z then + z = CarrierCommand.getCarrierOfUnit(firstUnit) + end + + if group.product.missionType == 'supply_air' then + if z then + z:capture(gr:getCoalition()) + z:addResource(group.product.cost) + env.info('GroupMonitor: processAir ['..group.name..'] has supplied ['..z.name..'] with ['..group.product.cost..']') + end + else + if z and z.side == gr:getCoalition() then + local percentSurvived = gr:getSize()/gr:getInitialSize() + local torecover = math.floor(group.product.cost * percentSurvived * GroupMonitor.recoveryReduction) + z:addResource(torecover) + env.info('GroupMonitor: processAir ['..z.name..'] has recovered ['..torecover..'] from ['..group.name..']') + end + end + else + env.info('GroupMonitor: processAir ['..group.name..'] size ['..gr:getSize()..'] has no unit 1') + end + else + if GroupMonitor.isAirAttack(group.product.missionType) and not group.returning then + if not GroupMonitor.hasWeapons(gr) then + env.info('GroupMonitor: processAir ['..group.name..'] size ['..gr:getSize()..'] has no weapons outside of shells') + self:sendHome(group) + elseif group.product.missionType == ZoneCommand.missionTypes.cas_helo then + local frUnit = gr:getUnit(1) + local controller = frUnit:getController() + local targets = controller:getDetectedTargets() + + local tgtToEngage = {} + if #targets > 0 then + for _,tgt in ipairs(targets) do + if tgt.visible and tgt.object and tgt.object.isExist and tgt.object:isExist() then + if Object.getCategory(tgt.object) == Object.Category.UNIT and + tgt.object.getCoalition and tgt.object:getCoalition()~=frUnit:getCoalition() and + Unit.getCategoryEx(tgt.object) == Unit.Category.GROUND_UNIT then + + local dist = mist.utils.get3DDist(frUnit:getPoint(), tgt.object:getPoint()) + if dist < 2000 then + table.insert(tgtToEngage, tgt.object) + end + end + end + end + end + + if not group.isengaging and #tgtToEngage > 0 then + env.info('GroupMonitor: processAir ['..group.name..'] engaging targets') + TaskExtensions.heloEngageTargets(gr, tgtToEngage, group.product.expend) + group.isengaging = true + group.startedEngaging = timer.getAbsTime() + elseif group.isengaging and #tgtToEngage == 0 and group.startedEngaging and (timer.getAbsTime() - group.startedEngaging) > 60*5 then + env.info('GroupMonitor: processAir ['..group.name..'] resuming mission') + if group.returning then + group.returning = nil + self:sendHome(group) + else + local homePos = group.home.zone.point + TaskExtensions.executeHeloCasMission(gr, group.target.built, group.product.expend, group.product.altitude, {homePos = homePos}) + end + group.isengaging = false + end + end + elseif group.product.missionType == 'supply_air' then + if not group.returning and group.target and group.target.side ~= group.product.side and group.target.side ~= 0 then + local supplyPoint = trigger.misc.getZone(group.home.name..'-hsp') + if not supplyPoint then + supplyPoint = trigger.misc.getZone(group.home.name) + end + + if supplyPoint then + group.returning = true + local alt = DependencyManager.get("ConnectionManager"):getHeliAlt(group.target.name, group.home.name) + TaskExtensions.landAtPointFromAir(gr, {x=supplyPoint.point.x, y=supplyPoint.point.z}, alt) + env.info('GroupMonitor: processAir ['..group.name..'] returning home') + end + end + end + end + end + elseif group.state =='landed' then + if timer.getAbsTime() - group.lastStateTime > GroupMonitor.landedDespawnTime then + if gr then + env.info('GroupMonitor: processAir ['..group.name..'] despawned after landing') + gr:destroy() + return true + end + end + end + end +end + +-----------------[[ END OF GroupMonitor.lua ]]----------------- + + + +-----------------[[ ConnectionManager.lua ]]----------------- + +ConnectionManager = {} +do + ConnectionManager.currentLineIndex = 5000 + function ConnectionManager:new() + local obj = {} + obj.connections = {} + obj.zoneConnections = {} + obj.heliAlts = {} + obj.blockedRoads = {} + setmetatable(obj, self) + self.__index = self + + DependencyManager.register("ConnectionManager", obj) + return obj + end + + function ConnectionManager:addConnection(f, t, blockedRoad, heliAlt) + local i = ConnectionManager.currentLineIndex + ConnectionManager.currentLineIndex = ConnectionManager.currentLineIndex + 1 + + table.insert(self.connections, {from=f, to=t, index=i}) + self.zoneConnections[f] = self.zoneConnections[f] or {} + self.zoneConnections[t] = self.zoneConnections[t] or {} + self.zoneConnections[f][t] = true + self.zoneConnections[t][f] = true + + if heliAlt then + self.heliAlts[f] = self.heliAlts[f] or {} + self.heliAlts[t] = self.heliAlts[t] or {} + self.heliAlts[f][t] = heliAlt + self.heliAlts[t][f] = heliAlt + end + + if blockedRoad then + self.blockedRoads[f] = self.blockedRoads[f] or {} + self.blockedRoads[t] = self.blockedRoads[t] or {} + self.blockedRoads[f][t] = true + self.blockedRoads[t][f] = true + end + + local from = CustomZone:getByName(f) + local to = CustomZone:getByName(t) + + if not from then env.info("ConnectionManager - addConnection: missing zone "..f) end + if not to then env.info("ConnectionManager - addConnection: missing zone "..t) end + + if blockedRoad then + trigger.action.lineToAll(-1, i, from.point, to.point, {1,1,1,0.5}, 3) + else + trigger.action.lineToAll(-1, i, from.point, to.point, {1,1,1,0.5}, 2) + end + end + + function ConnectionManager:getConnectionsOfZone(zonename) + if not self.zoneConnections[zonename] then return {} end + + local connections = {} + for i,v in pairs(self.zoneConnections[zonename]) do + table.insert(connections, i) + end + + return connections + end + + function ConnectionManager:isRoadBlocked(f,t) + if self.blockedRoads[f] then + return self.blockedRoads[f][t] + end + + if self.blockedRoads[t] then + return self.blockedRoads[t][f] + end + end + + function ConnectionManager:getHeliAltSimple(f,t) + if self.heliAlts[f] then + if self.heliAlts[f][t] then + return self.heliAlts[f][t] + end + end + + if self.heliAlts[t] then + if self.heliAlts[t][f] then + return self.heliAlts[t][f] + end + end + end + + function ConnectionManager:getHeliAlt(f,t) + local alt = self:getHeliAltSimple(f,t) + if alt then return alt end + + if self.heliAlts[f] then + local max = -1 + for zn,_ in pairs(self.heliAlts[f]) do + local alt = self:getHeliAltSimple(f, zn) + if alt then + if alt > max then + max = alt + end + end + + alt = self:getHeliAltSimple(zn, t) + if alt then + if alt > max then + max = alt + end + end + end + + if max > 0 then return max end + end + + if self.heliAlts[t] then + local max = -1 + for zn,_ in pairs(self.heliAlts[t]) do + local alt = self:getHeliAltSimple(t, zn) + if alt then + if alt > max then + max = alt + end + end + + alt = self:getHeliAltSimple(zn, f) + if alt then + if alt > max then + max = alt + end + end + end + + if max > 0 then return max end + end + end +end + +-----------------[[ END OF ConnectionManager.lua ]]----------------- + + + +-----------------[[ TaskExtensions.lua ]]----------------- + +TaskExtensions = {} +do + function TaskExtensions.getAttackTask(targetName, expend, altitude) + local tgt = Group.getByName(targetName) + if tgt then + return { + id = 'AttackGroup', + params = { + groupId = tgt:getID(), + expend = expend, + weaponType = Weapon.flag.AnyWeapon, + groupAttack = true, + altitudeEnabled = (altitude ~= nil), + altitude = altitude + } + } + else + tgt = StaticObject.getByName(targetName) + if not tgt then tgt = Unit.getByName(targetName) end + if tgt then + return { + id = 'AttackUnit', + params = { + unitId = tgt:getID(), + expend = expend, + weaponType = Weapon.flag.AnyWeapon, + groupAttack = true, + altitudeEnabled = (altitude ~= nil), + altitude = altitude + } + } + end + end + end + + function TaskExtensions.getTargetPos(targetName) + local tgt = StaticObject.getByName(targetName) + if not tgt then tgt = Unit.getByName(targetName) end + if tgt then + return tgt:getPoint() + end + end + + function TaskExtensions.getDefaultWaypoints(startPos, task, tgpos, reactivated, landUnitID) + local defwp = { + id='Mission', + params = { + route = { + airborne = true, + points = {} + } + } + } + + if reactivated then + table.insert(defwp.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = reactivated.currentPos.x, + y = reactivated.currentPos.z, + speed = 257, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = 4572, + alt_type = AI.Task.AltitudeType.BARO, + task = task + }) + else + table.insert(defwp.params.route.points, { + type= AI.Task.WaypointType.TAKEOFF, + x = startPos.x, + y = startPos.z, + speed = 0, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO + }) + + table.insert(defwp.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = startPos.x, + y = startPos.z, + speed = 257, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = 4572, + alt_type = AI.Task.AltitudeType.BARO, + task = task + }) + end + + if tgpos then + table.insert(defwp.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = tgpos.x, + y = tgpos.z, + speed = 257, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = 4572, + alt_type = AI.Task.AltitudeType.BARO, + task = task + }) + end + + if landUnitID then + table.insert(defwp.params.route.points, { + type= AI.Task.WaypointType.LAND, + linkUnit = landUnitID, + helipadId = landUnitID, + x = startPos.x, + y = startPos.z, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO + }) + else + table.insert(defwp.params.route.points, { + type= AI.Task.WaypointType.LAND, + x = startPos.x, + y = startPos.z, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO + }) + end + + return defwp + end + + function TaskExtensions.executeSeadMission(group,targets, expend, altitude, reactivated) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + if reactivated then + reactivated.currentPos = startPos + startPos = reactivated.homePos + end + + local expCount = AI.Task.WeaponExpend.ALL + if expend then + expCount = expend + end + + local alt = 4572 + if altitude then + alt = altitude/3.281 + end + + local viable = {} + for i,v in pairs(targets) do + if v.type == 'defense' and v.side ~= group:getCoalition() then + local gr = Group.getByName(v.name) + for _,unit in ipairs(gr:getUnits()) do + if unit:hasAttribute('SAM SR') or unit:hasAttribute('SAM TR') then + table.insert(viable, unit:getName()) + end + end + end + end + + local attack = { + id = 'ComboTask', + params = { + tasks = { + { + id = 'EngageTargets', + params = { + targetTypes = {'SAM SR', 'SAM TR'} + } + } + } + } + } + + for i,v in ipairs(viable) do + local task = TaskExtensions.getAttackTask(v, expCount, alt) + table.insert(attack.params.tasks, task) + end + + local firstunitpos = nil + local tgt = viable[1] + if tgt then + firstunitpos = Unit.getByName(tgt):getPoint() + end + + local mis = TaskExtensions.getDefaultWaypoints(startPos, attack, firstunitpos, reactivated) + + group:getController():setTask(mis) + TaskExtensions.setDefaultAG(group) + end + + function TaskExtensions.executeStrikeMission(group, targets, expend, altitude, reactivated, landUnitID) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + if reactivated then + reactivated.currentPos = startPos + startPos = reactivated.homePos + end + + local expCount = AI.Task.WeaponExpend.ALL + if expend then + expCount = expend + end + + local alt = 4572 + if altitude then + alt = altitude/3.281 + end + + local attack = { + id = 'ComboTask', + params = { + tasks = { + } + } + } + + for i,v in pairs(targets) do + if v.type == 'upgrade' and v.side ~= group:getCoalition() then + local task = TaskExtensions.getAttackTask(v.name, expCount, alt) + table.insert(attack.params.tasks, task) + end + end + + local mis = TaskExtensions.getDefaultWaypoints(startPos, attack, nil, reactivated, landUnitID) + + group:getController():setTask(mis) + TaskExtensions.setDefaultAG(group) + end + + function TaskExtensions.executePinpointStrikeMission(group, targetPos, expend, altitude, reactivated, landUnitID) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + if reactivated then + reactivated.currentPos = startPos + startPos = reactivated.homePos + end + + local expCount = AI.Task.WeaponExpend.ALL + if expend then + expCount = expend + end + + local alt = 4572 + if altitude then + alt = altitude/3.281 + end + + local attack = { + id = 'Bombing', + params = { + point = { + x = targetPos.x, + y = targetPos.z + }, + attackQty = 1, + weaponType = Weapon.flag.AnyBomb, + expend = expCount, + groupAttack = true, + altitude = alt, + altitudeEnabled = (altitude ~= nil), + } + } + + local diff = { + x = targetPos.x - startPos.x, + z = targetPos.z - startPos.z + } + + local tp = { + x = targetPos.x - diff.x*0.5, + z = targetPos.z - diff.z*0.5 + } + + local mis = TaskExtensions.getDefaultWaypoints(startPos, attack, tp, reactivated, landUnitID) + + group:getController():setTask(mis) + TaskExtensions.setDefaultAG(group) + end + + function TaskExtensions.executeCasMission(group, targets, expend, altitude, reactivated) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + if reactivated then + reactivated.currentPos = startPos + startPos = reactivated.homePos + end + + local attack = { + id = 'ComboTask', + params = { + tasks = { + } + } + } + + local expCount = AI.Task.WeaponExpend.ONE + if expend then + expCount = expend + end + + local alt = 4572 + if altitude then + alt = altitude/3.281 + end + + for i,v in pairs(targets) do + if v.type == 'defense' then + local g = Group.getByName(i) + if g and g:getCoalition()~=group:getCoalition() then + local task = TaskExtensions.getAttackTask(i, expCount, alt) + table.insert(attack.params.tasks, task) + end + end + end + + local mis = TaskExtensions.getDefaultWaypoints(startPos, attack, nil, reactivated) + + group:getController():setTask(mis) + TaskExtensions.setDefaultAG(group) + end + + function TaskExtensions.executeBaiMission(group, targets, expend, altitude, reactivated) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + if reactivated then + reactivated.currentPos = startPos + startPos = reactivated.homePos + end + + local attack = { + id = 'ComboTask', + params = { + tasks = { + { + id = 'EngageTargets', + params = { + targetTypes = {'Vehicles'} + } + } + } + } + } + + local expCount = AI.Task.WeaponExpend.ONE + if expend then + expCount = expend + end + + local alt = 4572 + if altitude then + alt = altitude/3.281 + end + + for i,v in pairs(targets) do + if v.type == 'mission' and (v.missionType == 'assault' or v.missionType == 'supply_convoy') then + local g = Group.getByName(i) + if g and g:getSize()>0 and g:getCoalition()~=group:getCoalition() then + local task = TaskExtensions.getAttackTask(i, expCount, alt) + table.insert(attack.params.tasks, task) + end + end + end + + local mis = TaskExtensions.getDefaultWaypoints(startPos, attack, nil, reactivated) + + group:getController():setTask(mis) + TaskExtensions.setDefaultAG(group) + end + + function TaskExtensions.heloEngageTargets(group, targets, expend) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + local attack = { + id = 'ComboTask', + params = { + tasks = { + } + } + } + + local expCount = AI.Task.WeaponExpend.ONE + if expend then + expCount = expend + end + + for i,v in pairs(targets) do + local task = { + id = 'AttackUnit', + params = { + unitId = v:getID(), + expend = expend, + weaponType = Weapon.flag.AnyWeapon, + groupAttack = true + } + } + + table.insert(attack.params.tasks, task) + end + + group:getController():pushTask(attack) + end + + function TaskExtensions.executeHeloCasMission(group, targets, expend, altitude, reactivated) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + if reactivated then + reactivated.currentPos = startPos + startPos = reactivated.homePos + end + + local attack = { + id = 'ComboTask', + params = { + tasks = { + } + } + } + + local expCount = AI.Task.WeaponExpend.ONE + if expend then + expCount = expend + end + + local alt = 61 + if altitude then + alt = altitude/3.281 + end + + for i,v in pairs(targets) do + if v.type == 'defense' then + local g = Group.getByName(i) + if g and g:getCoalition()~=group:getCoalition() then + local task = TaskExtensions.getAttackTask(i, expCount, alt) + table.insert(attack.params.tasks, task) + end + end + end + + local land = { + id='Land', + params = { + point = {x = startPos.x, y=startPos.z} + } + } + + local mis = { + id='Mission', + params = { + route = { + airborne = true, + points = {} + } + } + } + + if reactivated then + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = reactivated.currentPos.x+1000, + y = reactivated.currentPos.z+1000, + speed = 257, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.RADIO, + task = attack + }) + else + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TAKEOFF, + x = startPos.x, + y = startPos.z, + speed = 0, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO, + }) + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = startPos.x+1000, + y = startPos.z+1000, + speed = 257, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.RADIO, + task = attack + }) + end + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = startPos.x, + y = startPos.z, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.RADIO, + task = land + }) + + group:getController():setTask(mis) + TaskExtensions.setDefaultAG(group) + end + + function TaskExtensions.executeTankerMission(group, point, altitude, frequency, tacan, reactivated, landUnitID) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + if reactivated then + reactivated.currentPos = startPos + startPos = reactivated.homePos + end + + local alt = 4572 + if altitude then + alt = altitude/3.281 + end + + local freq = 259500000 + if frequency then + freq = math.floor(frequency*1000000) + end + + local setfreq = { + id = 'SetFrequency', + params = { + frequency = freq, + modulation = 0 + } + } + + local setbeacon = { + id = 'ActivateBeacon', + params = { + type = 4, -- TACAN type + system = 4, -- Tanker TACAN + name = 'tacan task', + callsign = group:getUnit(1):getCallsign():sub(1,3), + frequency = tacan, + AA = true, + channel = tacan, + bearing = true, + modeChannel = "X" + } + } + + local distFromPoint = 20000 + local theta = math.random() * 2 * math.pi + + local dx = distFromPoint * math.cos(theta) + local dy = distFromPoint * math.sin(theta) + + local pos1 = { + x = point.x + dx, + y = point.z + dy + } + + local pos2 = { + x = point.x - dx, + y = point.z - dy + } + + local orbit_speed = 97 + local travel_speed = 450 + + local orbit = { + id = 'Orbit', + params = { + pattern = AI.Task.OrbitPattern.RACE_TRACK, + point = pos1, + point2 = pos2, + speed = orbit_speed, + altitude = alt + } + } + + local script = { + id = "WrappedAction", + params = { + action = { + id = "Script", + params = + { + command = "trigger.action.outTextForCoalition("..group:getCoalition()..", 'Tanker on station. "..(freq/1000000).." AM', 15)", + } + } + } + } + + local tanker = { + id = 'Tanker', + params = { + } + } + + local task = { + id='Mission', + params = { + route = { + airborne = true, + points = {} + } + } + } + + if reactivated then + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = pos1.x, + y = pos1.y, + speed = travel_speed, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.BARO, + task = tanker + }) + else + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TAKEOFF, + x = startPos.x, + y = startPos.z, + speed = 0, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO, + task = tanker + }) + end + + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = pos1.x, + y = pos1.y, + speed = orbit_speed, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.BARO, + task = { + id = 'ComboTask', + params = { + tasks = { + script + } + } + } + }) + + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = pos2.x, + y = pos2.y, + speed = orbit_speed, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.BARO, + task = { + id = 'ComboTask', + params = { + tasks = { + orbit + } + } + } + }) + + if landUnitID then + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.LAND, + linkUnit = landUnitID, + helipadId = landUnitID, + x = startPos.x, + y = startPos.z, + speed = travel_speed, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO + }) + else + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.LAND, + x = startPos.x, + y = startPos.z, + speed = travel_speed, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO + }) + end + + group:getController():setTask(task) + group:getController():setOption(AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE) + group:getController():setCommand(setfreq) + group:getController():setCommand(setbeacon) + end + + function TaskExtensions.executeAwacsMission(group, point, altitude, frequency, reactivated, landUnitID) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + if reactivated then + reactivated.currentPos = startPos + startPos = reactivated.homePos + end + + local alt = 4572 + if altitude then + alt = altitude/3.281 + end + + local freq = 259500000 + if frequency then + freq = math.floor(frequency*1000000) + end + + local setfreq = { + id = 'SetFrequency', + params = { + frequency = freq, + modulation = 0 + } + } + + local distFromPoint = 10000 + local theta = math.random() * 2 * math.pi + + local dx = distFromPoint * math.cos(theta) + local dy = distFromPoint * math.sin(theta) + + local pos1 = { + x = point.x + dx, + y = point.z + dy + } + + local pos2 = { + x = point.x - dx, + y = point.z - dy + } + + local orbit = { + id = 'Orbit', + params = { + pattern = AI.Task.OrbitPattern.RACE_TRACK, + point = pos1, + point2 = pos2, + altitude = alt + } + } + + local script = { + id = "WrappedAction", + params = { + action = { + id = "Script", + params = + { + command = "trigger.action.outTextForCoalition("..group:getCoalition()..", 'AWACS on station. "..(freq/1000000).." AM', 15)", + } + } + } + } + + + local awacs = { + id = 'ComboTask', + params = { + tasks = { + { + id = "WrappedAction", + params = + { + action = + { + id = "EPLRS", + params = { + value = true, + groupId = group:getID(), + } + } + } + }, + { + id = 'AWACS', + params = { + } + } + } + } + } + + local task = { + id='Mission', + params = { + route = { + airborne = true, + points = {} + } + } + } + + if reactivated then + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = pos1.x, + y = pos1.y, + speed = 257, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.BARO, + task = awacs + }) + else + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TAKEOFF, + x = startPos.x, + y = startPos.z, + speed = 0, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO, + task = awacs + }) + end + + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = pos1.x, + y = pos1.y, + speed = 257, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.BARO, + task = { + id = 'ComboTask', + params = { + tasks = { + script + } + } + } + }) + + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = pos2.x, + y = pos2.y, + speed = 257, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.BARO, + task = { + id = 'ComboTask', + params = { + tasks = { + orbit + } + } + } + }) + + if landUnitID then + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.LAND, + linkUnit = landUnitID, + helipadId = landUnitID, + x = startPos.x, + y = startPos.z, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO + }) + else + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.LAND, + x = startPos.x, + y = startPos.z, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO + }) + end + + group:getController():setTask(task) + group:getController():setOption(AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE) + group:getController():setCommand(setfreq) + end + + function TaskExtensions.executePatrolMission(group, point, altitude, range, reactivated, landUnitID) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + if reactivated then + reactivated.currentPos = startPos + startPos = reactivated.homePos + end + + local rng = 25 * 1852 + if range then + rng = range * 1852 + end + + local alt = 4572 + if altitude then + alt = altitude/3.281 + end + + local search = { + id = 'EngageTargets', + params = { + maxDist = rng, + targetTypes = { 'Planes', 'Helicopters' } + } + } + + local distFromPoint = 10000 + local theta = math.random() * 2 * math.pi + + local dx = distFromPoint * math.cos(theta) + local dy = distFromPoint * math.sin(theta) + + local p1 = { + x = point.x + dx, + y = point.z + dy + } + + local p2 = { + x = point.x - dx, + y = point.z - dy + } + + local orbit = { + id = 'Orbit', + params = { + pattern = AI.Task.OrbitPattern.RACE_TRACK, + point = p1, + point2 = p2, + speed = 154, + altitude = alt + } + } + + local task = { + id='Mission', + params = { + route = { + airborne = true, + points = {} + } + } + } + + if not reactivated then + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TAKEOFF, + x = startPos.x, + y = startPos.z, + speed = 0, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO, + task = search + }) + else + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = reactivated.currentPos.x, + y = reactivated.currentPos.z, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.BARO, + task = search + }) + end + + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = p1.x, + y = p1.y, + speed = 257, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.BARO + }) + + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = p2.x, + y = p2.y, + speed = 257, + action = AI.Task.TurnMethod.FLY_OVER_POINT, + alt = alt, + alt_type = AI.Task.AltitudeType.BARO, + task = orbit + }) + + if landUnitID then + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.LAND, + linkUnit = landUnitID, + helipadId = landUnitID, + x = startPos.x, + y = startPos.z, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO + }) + else + table.insert(task.params.route.points, { + type= AI.Task.WaypointType.LAND, + x = startPos.x, + y = startPos.z, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO + }) + end + + group:getController():setTask(task) + TaskExtensions.setDefaultAA(group) + end + + function TaskExtensions.setDefaultAA(group) + group:getController():setOption(AI.Option.Air.id.PROHIBIT_AG, true) + group:getController():setOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY, true) + group:getController():setOption(AI.Option.Air.id.PROHIBIT_JETT, true) + group:getController():setOption(AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) + group:getController():setOption(AI.Option.Air.id.MISSILE_ATTACK, AI.Option.Air.val.MISSILE_ATTACK.MAX_RANGE) + + local weapons = 268402688 -- AnyMissile + group:getController():setOption(AI.Option.Air.id.RTB_ON_OUT_OF_AMMO, weapons) + end + + function TaskExtensions.setDefaultAG(group) + --group:getController():setOption(AI.Option.Air.id.PROHIBIT_AA, true) + group:getController():setOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY, true) + group:getController():setOption(AI.Option.Air.id.PROHIBIT_JETT, true) + group:getController():setOption(AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) + + local weapons = 2147485694 + 30720 + 4161536 -- AnyBomb + AnyRocket + AnyASM + group:getController():setOption(AI.Option.Air.id.RTB_ON_OUT_OF_AMMO, weapons) + end + + function TaskExtensions.stopAndDisperse(group) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local pos = group:getUnit(1):getPoint() + group:getController():setTask({ + id='Mission', + params = { + route = { + points = { + [1] = { + type= AI.Task.WaypointType.TURNING_POINT, + x = pos.x, + y = pos.z, + speed = 1000, + action = AI.Task.VehicleFormation.OFF_ROAD + }, + [2] = { + type= AI.Task.WaypointType.TURNING_POINT, + x = pos.x+math.random(25), + y = pos.z+math.random(25), + speed = 1000, + action = AI.Task.VehicleFormation.DIAMOND + }, + } + } + } + }) + end + + function TaskExtensions.moveOnRoadToPointAndAssault(group, point, targets, detour) + if not group or not point then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + local srx, sry = land.getClosestPointOnRoads('roads', startPos.x, startPos.z) + local erx, ery = land.getClosestPointOnRoads('roads', point.x, point.y) + + local mis = { + id='Mission', + params = { + route = { + points = {} + } + } + } + + if detour then + local detourPoint = {x = startPos.x, y = startPos.z} + + local direction = { + x = erx - startPos.x, + y = ery - startPos.y + } + + local magnitude = (direction.x^2 + direction.y^2) ^ 0.5 + if magnitude > 0.0 then + direction.x = direction.x / magnitude + direction.y = direction.y / magnitude + + local scale = math.random(250,500) + direction.x = direction.x * scale + direction.y = direction.y * scale + + detourPoint.x = detourPoint.x + direction.x + detourPoint.y = detourPoint.y + direction.y + else + detourPoint.x = detourPoint.x + math.random(-500,500) + detourPoint.y = detourPoint.y + math.random(-500,500) + end + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = detourPoint.x, + y = detourPoint.y, + speed = 1000, + action = AI.Task.VehicleFormation.OFF_ROAD + }) + + srx, sry = land.getClosestPointOnRoads('roads', detourPoint.x, detourPoint.y) + end + + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = srx, + y = sry, + speed = 1000, + action = AI.Task.VehicleFormation.ON_ROAD + }) + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = erx, + y = ery, + speed = 1000, + action = AI.Task.VehicleFormation.ON_ROAD + }) + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = point.x, + y = point.y, + speed = 1000, + action = AI.Task.VehicleFormation.DIAMOND + }) + + + for i,v in pairs(targets) do + if v.type == 'defense' then + local group = Group.getByName(v.name) + if group then + for i,v in ipairs(group:getUnits()) do + local unpos = v:getPoint() + local pnt = {x=unpos.x, y = unpos.z} + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = pnt.x, + y = pnt.y, + speed = 10, + action = AI.Task.VehicleFormation.DIAMOND + }) + end + end + end + end + + group:getController():setTask(mis) + end + + function TaskExtensions.moveOnRoadToPoint(group, point, detour) -- point = {x,y} + if not group or not point then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + local srx, sry = land.getClosestPointOnRoads('roads', startPos.x, startPos.z) + local erx, ery = land.getClosestPointOnRoads('roads', point.x, point.y) + + local mis = { + id='Mission', + params = { + route = { + points = { + } + } + } + } + + if detour then + local detourPoint = {x = startPos.x, y = startPos.z} + + local direction = { + x = erx - startPos.x, + y = ery - startPos.y + } + + local magnitude = (direction.x^2 + direction.y^2) ^ 0.5 + if magnitude > 0.0 then + direction.x = direction.x / magnitude + direction.y = direction.y / magnitude + + local scale = math.random(250,1000) + direction.x = direction.x * scale + direction.y = direction.y * scale + + detourPoint.x = detourPoint.x + direction.x + detourPoint.y = detourPoint.y + direction.y + else + detourPoint.x = detourPoint.x + math.random(-500,500) + detourPoint.y = detourPoint.y + math.random(-500,500) + end + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = detourPoint.x, + y = detourPoint.y, + speed = 1000, + action = AI.Task.VehicleFormation.OFF_ROAD + }) + + srx, sry = land.getClosestPointOnRoads('roads', detourPoint.x, detourPoint.y) + end + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = srx, + y = sry, + speed = 1000, + action = AI.Task.VehicleFormation.ON_ROAD + }) + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = erx, + y = ery, + speed = 1000, + action = AI.Task.VehicleFormation.ON_ROAD + }) + + table.insert(mis.params.route.points, { + type= AI.Task.WaypointType.TURNING_POINT, + x = point.x, + y = point.y, + speed = 1000, + action = AI.Task.VehicleFormation.OFF_ROAD + }) + + group:getController():setTask(mis) + end + + function TaskExtensions.landAtPointFromAir(group, point, alt) + if not group or not point then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + local atype = AI.Task.AltitudeType.RADIO + if alt then + atype = AI.Task.AltitudeType.BARO + else + alt = 500 + end + + local land = { + id='Land', + params = { + point = point + } + } + + local mis = { + id='Mission', + params = { + route = { + airborne = true, + points = { + [1] = { + type= AI.Task.WaypointType.TURNING_POINT, + x = startPos.x, + y = startPos.z, + speed = 500, + action = AI.Task.TurnMethod.FIN_POINT, + alt = alt, + alt_type = atype + }, + [2] = { + type= AI.Task.WaypointType.TURNING_POINT, + x = point.x, + y = point.y, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = alt, + alt_type = atype, + task = land + } + } + } + } + } + + group:getController():setTask(mis) + end + + function TaskExtensions.landAtPoint(group, point, alt, skiptakeoff) -- point = {x,y} + if not group or not point then return end + if not group:isExist() or group:getSize()==0 then return end + local startPos = group:getUnit(1):getPoint() + + local atype = AI.Task.AltitudeType.RADIO + if alt then + atype = AI.Task.AltitudeType.BARO + else + alt = 500 + end + + local land = { + id='Land', + params = { + point = point + } + } + + local mis = { + id='Mission', + params = { + route = { + airborne = true, + points = {} + } + } + } + + if not skiptakeoff then + table.insert(mis.params.route.points,{ + type = AI.Task.WaypointType.TAKEOFF, + x = startPos.x, + y = startPos.z, + speed = 0, + action = AI.Task.TurnMethod.FIN_POINT, + alt = alt, + alt_type = atype + }) + end + + table.insert(mis.params.route.points,{ + type = AI.Task.WaypointType.TURNING_POINT, + x = point.x, + y = point.y, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = alt, + alt_type = atype, + task = land + }) + + group:getController():setTask(mis) + end + + function TaskExtensions.landAtAirfield(group, point) -- point = {x,y} + if not group or not point then return end + if not group:isExist() or group:getSize()==0 then return end + + local mis = { + id='Mission', + params = { + route = { + airborne = true, + points = { + [1] = { + type= AI.Task.WaypointType.TURNING_POINT, + x = point.x, + y = point.z, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 4572, + alt_type = AI.Task.AltitudeType.BARO + }, + [2] = { + type= AI.Task.WaypointType.LAND, + x = point.x, + y = point.z, + speed = 257, + action = AI.Task.TurnMethod.FIN_POINT, + alt = 0, + alt_type = AI.Task.AltitudeType.RADIO + } + } + } + } + } + + group:getController():setTask(mis) + end + + function TaskExtensions.fireAtTargets(group, targets, amount) + if not group then return end + if not group:isExist() or group:getSize() == 0 then return end + + local units = {} + for i,v in pairs(targets) do + local g = Group.getByName(v.name) + if g then + for i2,v2 in ipairs(g:getUnits()) do + table.insert(units, v2) + end + else + local s = StaticObject.getByName(v.name) + if s then + table.insert(units, s) + end + end + end + + if #units == 0 then + return + end + + local selected = {} + for i=1,amount,1 do + if #units == 0 then + break + end + + local tgt = math.random(1,#units) + + table.insert(selected, units[tgt]) + table.remove(units, tgt) + end + + while #selected < amount do + local ind = math.random(1,#selected) + table.insert(selected, selected[ind]) + end + + for i,v in ipairs(selected) do + local unt = v + if unt then + local target = {} + target.x = unt:getPosition().p.x + target.y = unt:getPosition().p.z + target.radius = 100 + target.expendQty = 1 + target.expendQtyEnabled = true + local fire = {id = 'FireAtPoint', params = target} + + group:getController():pushTask(fire) + end + end + end + + function TaskExtensions.carrierGoToPos(group, point) + if not group or not point then return end + if not group:isExist() or group:getSize()==0 then return end + + local mis = { + id='Mission', + params = { + route = { + airborne = true, + points = { + [1] = { + type= AI.Task.WaypointType.TURNING_POINT, + x = point.x, + y = point.z, + speed = 50, + action = AI.Task.TurnMethod.FIN_POINT + } + } + } + } + } + + group:getController():setTask(mis) + end + + function TaskExtensions.stopCarrier(group) + if not group then return end + if not group:isExist() or group:getSize()==0 then return end + local point = group:getUnit(1):getPoint() + + group:getController():setTask({ + id='Mission', + params = { + route = { + airborne = false, + points = { + [1] = { + type= AI.Task.WaypointType.TURNING_POINT, + x = point.x, + y = point.z, + speed = 0, + action = AI.Task.TurnMethod.FIN_POINT + } + } + } + } + }) + end + + function TaskExtensions.setupCarrier(unit, icls, acls, tacan, link4, radio) + if not unit then return end + if not unit:isExist() then return end + + local commands = {} + if icls then + table.insert(commands, { + id = 'ActivateICLS', + params = { + type = 131584, + channel = icls, + unitId = unit:getID(), + name = "ICLS "..icls, + } + }) + end + + if acls then + table.insert(commands, { + id = 'ActivateACLS', + params = { + unitId = unit:getID(), + name = "ACLS", + } + }) + end + + if tacan then + table.insert(commands, { + id = 'ActivateBeacon', + params = { + type = 4, + system = 4, + name = "TACAN "..tacan.channel, + callsign = tacan.callsign, + frequency = tacan.channel, + channel = tacan.channel, + bearing = true, + modeChannel = "X" + } + }) + end + + if link4 then + table.insert(commands, { + id = 'ActivateLink4', + params = { + unitId = unit:getID(), + frequency = link4, + name = "Link4 "..link4, + } + }) + end + + if radio then + table.insert(commands, { + id = "SetFrequency", + params = { + power = 100, + modulation = 0, + frequency = radio, + } + }) + end + + for i,v in ipairs(commands) do + unit:getController():setCommand(v) + end + + unit:getGroup():getController():setOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION, 30) + end +end + +-----------------[[ END OF TaskExtensions.lua ]]----------------- + + + +-----------------[[ MarkerCommands.lua ]]----------------- + +MarkerCommands = {} +do + function MarkerCommands:new() + local obj = {} + obj.commands = {} --{command=string, action=function} + + setmetatable(obj, self) + self.__index = self + + obj:start() + + DependencyManager.register("MarkerCommands", obj) + return obj + end + + function MarkerCommands:addCommand(command, action, hasParam, state) + table.insert(self.commands, {command = command, action = action, hasParam = hasParam, state = state}) + end + + function MarkerCommands:start() + local markEditedEvent = {} + markEditedEvent.context = self + function markEditedEvent:onEvent(event) + if event.id == 26 and event.text and (event.coalition == 1 or event.coalition == 2) then -- mark changed + local success = false + env.info('MarkerCommands - input: '..event.text) + + for i,v in ipairs(self.context.commands) do + if (not v.hasParam) and event.text == v.command then + success = v.action(event, nil, v.state) + break + elseif v.hasParam and event.text:find('^'..v.command..':') then + local param = event.text:gsub('^'..v.command..':', '') + success = v.action(event, param, v.state) + break + end + end + + if success then + trigger.action.removeMark(event.idx) + end + end + end + + world.addEventHandler(markEditedEvent) + end +end + + +-----------------[[ END OF MarkerCommands.lua ]]----------------- + + + +-----------------[[ ZoneCommand.lua ]]----------------- + +ZoneCommand = {} +do + ZoneCommand.currentZoneIndex = 1000 + ZoneCommand.allZones = {} + ZoneCommand.buildSpeed = Config.buildSpeed + ZoneCommand.supplyBuildSpeed = Config.supplyBuildSpeed + ZoneCommand.missionValidChance = 0.9 + ZoneCommand.missionBuildSpeedReduction = Config.missionBuildSpeedReduction + ZoneCommand.revealTime = 0 + ZoneCommand.staticRegistry = {} + + ZoneCommand.modes = { + normal = 'normal', + supply = 'supply', + export = 'export' + } + + ZoneCommand.productTypes = { + upgrade = 'upgrade', + mission = 'mission', + defense = 'defense' + } + + ZoneCommand.missionTypes = { + supply_air = 'supply_air', + supply_convoy = 'supply_convoy', + cas = 'cas', + cas_helo = 'cas_helo', + strike = 'strike', + patrol = 'patrol', + sead = 'sead', + assault = 'assault', + bai = 'bai', + supply_transfer = 'supply_transfer', + awacs = 'awacs', + tanker = 'tanker' + } + + function ZoneCommand:new(zonename) + local obj = {} + obj.name = zonename + obj.side = 0 + obj.resource = 0 + obj.resourceChange = 0 + obj.maxResource = 20000 + obj.spendTreshold = 5000 + obj.keepActive = false + obj.boostScale = 1.0 + obj.extraBuildResources = 0 + obj.reservedMissions = {} + obj.isHeloSpawn = false + obj.isPlaneSpawn = false + obj.spawnSurface = nil + + obj.zone = CustomZone:getByName(zonename) + obj.products = {} + obj.mode = 'normal' + --[[ + normal: buys whatever it can + supply: buys only supply missions + export: supply mode, but also sells all defense groups from the zone + ]]-- + obj.index = ZoneCommand.currentZoneIndex + ZoneCommand.currentZoneIndex = ZoneCommand.currentZoneIndex + 1 + + obj.built = {} + obj.income = 0 + + --group restrictions + obj.spawns = {} + for i,v in pairs(mist.DBs.groupsByName) do + if v.units[1].skill == 'Client' then + local zn = obj.zone + local pos3d = { + x = v.units[1].point.x, + y = 0, + z = v.units[1].point.y + } + + if zn and zn:isInside(pos3d) then + local coa = 0 + if v.coalition=='blue' then + coa = 2 + elseif v.coalition=='red' then + coa = 1 + end + + table.insert(obj.spawns, {name=i, side=coa}) + end + end + end + + --draw graphics + local color = {0.7,0.7,0.7,0.3} + if obj.side == 1 then + color = {1,0,0,0.3} + elseif obj.side == 2 then + color = {0,0,1,0.3} + end + + obj.zone:draw(obj.index, color, color) + + local point = obj.zone.point + + if obj.zone:isCircle() then + point = { + x = obj.zone.point.x, + y = obj.zone.point.y, + z = obj.zone.point.z + obj.zone.radius + } + elseif obj.zone:isQuad() then + local largestZ = obj.zone.vertices[1].z + local largestX = obj.zone.vertices[1].x + for i=2,4,1 do + if obj.zone.vertices[i].z > largestZ then + largestZ = obj.zone.vertices[i].z + largestX = obj.zone.vertices[i].x + end + end + + point = { + x = largestX, + y = obj.zone.point.y, + z = largestZ + } + end + + --trigger.action.textToAll(1,1000+obj.index,point, {0,0,0,0.8}, {1,1,1,0.5}, 15, true, '') + --trigger.action.textToAll(2,2000+obj.index,point, {0,0,0,0.8}, {1,1,1,0.5}, 15, true, '') + trigger.action.textToAll(-1,2000+obj.index,point, {0,0,0,0.8}, {1,1,1,0.5}, 15, true, '') --show blue to all + setmetatable(obj, self) + self.__index = self + + obj:refreshText() + obj:start() + obj:refreshSpawnBlocking() + ZoneCommand.allZones[obj.name] = obj + return obj + end + + function ZoneCommand:refreshSpawnBlocking() + for _,v in ipairs(self.spawns) do + local isDifferentSide = v.side ~= self.side + local noResources = self.resource < Config.zoneSpawnCost + + trigger.action.setUserFlag(v.name, isDifferentSide or noResources) + end + end + + function ZoneCommand.setNeighbours() + local conManager = DependencyManager.get("ConnectionManager") + for name,zone in pairs(ZoneCommand.allZones) do + local neighbours = conManager:getConnectionsOfZone(name) + zone.neighbours = {} + for _,zname in ipairs(neighbours) do + zone.neighbours[zname] = ZoneCommand.getZoneByName(zname) + end + end + end + + function ZoneCommand.getZoneByName(name) + if not name then return nil end + return ZoneCommand.allZones[name] + end + + function ZoneCommand.getAllZones() + return ZoneCommand.allZones + end + + function ZoneCommand.getZoneOfUnit(unitname) + local un = Unit.getByName(unitname) + + if not un then + return nil + end + + for i,v in pairs(ZoneCommand.allZones) do + if Utils.isInZone(un, i) then + return v + end + end + + return nil + end + + function ZoneCommand.getZoneOfWeapon(weapon) + if not weapon then + return nil + end + + for i,v in pairs(ZoneCommand.allZones) do + if Utils.isInZone(weapon, i) then + return v + end + end + + return nil + end + + function ZoneCommand.getClosestZoneToPoint(point, side) + local minDist = 9999999 + local closest = nil + for i,v in pairs(ZoneCommand.allZones) do + if not side or side == v.side then + local d = mist.utils.get2DDist(v.zone.point, point) + if d < minDist then + minDist = d + closest = v + end + end + end + + return closest, minDist + end + + function ZoneCommand.getZoneOfPoint(point) + for i,v in pairs(ZoneCommand.allZones) do + local z = CustomZone:getByName(i) + if z and z:isInside(point) then + return v + end + end + + return nil + end + + function ZoneCommand:boostProduction(amount) + self.extraBuildResources = self.extraBuildResources + amount + env.info('ZoneCommand:boostProduction - '..self.name..' production boosted by '..amount..' to a total of '..self.extraBuildResources) + end + + function ZoneCommand:sabotage(explosionSize, sourcePoint) + local minDist = 99999999 + local closest = nil + for i,v in pairs(self.built) do + if v.type == 'upgrade' then + local st = StaticObject.getByName(v.name) + if not st then st = Group.getByName(v.name) end + local pos = st:getPoint() + + local d = mist.utils.get2DDist(pos, sourcePoint) + if d < minDist then + minDist = d; + closest = pos + end + end + end + + if closest then + trigger.action.explosion(closest, explosionSize) + env.info('ZoneCommand:sabotage - Structure has been sabotaged at '..self.name) + end + + local damagedResources = math.random(2000,5000) + self:removeResource(damagedResources) + self:refreshText() + end + + function ZoneCommand:refreshText() + local build = '' + if self.currentBuild then + local job = '' + local display = self.currentBuild.product.display + if self.currentBuild.product.type == 'upgrade' then + job = display + elseif self.currentBuild.product.type == 'defense' then + if self.currentBuild.isRepair then + job = display..' (repair)' + else + job = display + end + elseif self.currentBuild.product.type == 'mission' then + job = display + end + + build = '\n['..job..' '..math.min(math.floor((self.currentBuild.progress/self.currentBuild.product.cost)*100),100)..'%]' + end + + local mBuild = '' + if self.currentMissionBuild then + local job = '' + local display = self.currentMissionBuild.product.display + job = display + + mBuild = '\n['..job..' '..math.min(math.floor((self.currentMissionBuild.progress/self.currentMissionBuild.product.cost)*100),100)..'%]' + end + + local status='' + if self.side ~= 0 and self:criticalOnSupplies() then + status = '(!)' + end + + local color = {0.3,0.3,0.3,1} + if self.side == 1 then + color = {0.7,0,0,1} + elseif self.side == 2 then + color = {0,0,0.7,1} + end + + --trigger.action.setMarkupColor(1000+self.index, color) + trigger.action.setMarkupColor(2000+self.index, color) + + local label = '['..self.resource..'/'..self.maxResource..']'..status..build..mBuild + + if self.side == 1 then + --trigger.action.setMarkupText(1000+self.index, self.name..label) + + if self.revealTime > 0 then + trigger.action.setMarkupText(2000+self.index, self.name..label) + else + trigger.action.setMarkupText(2000+self.index, self.name) + end + elseif self.side == 2 then + --if self.revealTime > 0 then + -- trigger.action.setMarkupText(1000+self.index, self.name..label) + --else + -- trigger.action.setMarkupText(1000+self.index, self.name) + --end + trigger.action.setMarkupText(2000+self.index, self.name..label) + elseif self.side == 0 then + --trigger.action.setMarkupText(1000+self.index, ' '..self.name..' ') + trigger.action.setMarkupText(2000+self.index, ' '..self.name..' ') + end + end + + function ZoneCommand:setSide(side) + self.side = side + self:refreshSpawnBlocking() + + if side == 0 then + self.revealTime = 0 + end + + local color = {0.7,0.7,0.7,0.3} + if self.side==1 then + color = {1,0,0,0.3} + elseif self.side==2 then + color = {0,0,1,0.3} + end + + trigger.action.setMarkupColorFill(self.index, color) + trigger.action.setMarkupColor(self.index, color) + trigger.action.setMarkupTypeLine(self.index, 1) + + if self.side == 2 and (self.isHeloSpawn or self.isPlaneSpawn) then + trigger.action.setMarkupTypeLine(self.index, 2) + trigger.action.setMarkupColor(self.index, {0,1,0,1}) + end + + self:refreshText() + + if self.airbaseName then + local ab = Airbase.getByName(self.airbaseName) + if ab then + if ab:autoCaptureIsOn() then ab:autoCapture(false) end + ab:setCoalition(self.side) + else + for i=1,10,1 do + local ab = Airbase.getByName(self.airbaseName..'-'..i) + if ab then + if ab:autoCaptureIsOn() then ab:autoCapture(false) end + ab:setCoalition(self.side) + end + end + end + end + end + + function ZoneCommand:addResource(amount) + self.resource = self.resource+amount + self.resource = math.floor(math.min(self.resource, self.maxResource)) + self:refreshSpawnBlocking() + end + + function ZoneCommand:removeResource(amount) + self.resource = self.resource-amount + self.resource = math.floor(math.max(self.resource, 0)) + self:refreshSpawnBlocking() + end + + function ZoneCommand:reveal(time) + local revtime = 30 + if time then + revtime = time + end + + self.revealTime = 60*revtime + self:refreshText() + end + + function ZoneCommand:needsSupplies(sendamount) + return self.resource + sendamount= cost then + self:removeResource(cost) + else + break + end + end + + self:instantBuild(v) + + for i2,v2 in ipairs(v.products) do + if (v2.type == 'defense' or v2.type=='upgrade') and v2.cost > 0 then + if useCost then + local cost = v2.cost * useCost + if self.resource >= cost then + self:removeResource(cost) + else + break + end + end + + self:instantBuild(v2) + end + end + end + end + + function ZoneCommand:start() + timer.scheduleFunction(function(param, time) + local self = param.context + local initialRes = self.resource + + --generate income + if self.side ~= 0 then + self:addResource(self.income) + end + + --untrack destroyed zone upgrades + for i,v in pairs(self.built) do + local u = Group.getByName(i) + if u and u:getSize() == 0 then + u:destroy() + self.built[i] = nil + end + + if not u then + u = StaticObject.getByName(i) + if u and u:getLife()<1 then + u:destroy() + self.built[i] = nil + end + end + + if not u then + self.built[i] = nil + end + end + + --upkeep costs for defenses + for i,v in pairs(self.built) do + if v.type == 'defense' and v.upkeep then + v.strikes = v.strikes or 0 + if self.resource >= v.upkeep then + self:removeResource(v.upkeep) + v.strikes = 0 + else + if v.strikes < 6 then + v.strikes = v.strikes+1 + else + local u = Group.getByName(i) + if u then + v.strikes = nil + u:destroy() + self.built[i] = nil + end + end + end + elseif v.type == 'upgrade' and v.income then + self:addResource(v.income) + end + end + + --check if zone should be reverted to neutral + local hasUpgrade = false + for i,v in pairs(self.built) do + if v.type=='upgrade' then + hasUpgrade = true + break + end + end + + if not hasUpgrade and self.side ~= 0 then + local sidetxt = "Neutral" + if self.side == 1 then + sidetxt = "Red" + elseif self.side == 2 then + sidetxt = "Blue" + end + + trigger.action.outText(sidetxt.." has lost control of "..self.name, 15) + + self:setSide(0) + self.mode = 'normal' + self.currentBuild = nil + self.currentMissionBuild = nil + end + + --sell defenses if export mode + if self.side ~= 0 and self.mode == 'export' then + for i,v in pairs(self.built) do + if v.type=='defense' then + local g = Group.getByName(i) + if g then g:destroy() end + self:addResource(math.floor(v.cost/2)) + self.built[i] = nil + end + end + end + + self:verifyBuildValid() + self:chooseBuild() + self:progressBuild() + + self.resourceChange = self.resource - initialRes + self:refreshText() + + --use revealTime resource + if self.revealTime > 0 then + self.revealTime = math.max(0,self.revealTime-10) + end + + return time+10 + end, {context = self}, timer.getTime()+1) + end + + function ZoneCommand:verifyBuildValid() + if self.currentBuild then + if self.side == 0 then + self.currentBuild = nil + env.info('ZoneCommand:verifyBuildValid - stopping build, zone is neutral') + end + + if self.mode == 'export' or self.mode == 'supply' then + if not (self.currentBuild.product.type == ZoneCommand.productTypes.upgrade or + self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_air or + self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_convoy or + self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_transfer) then + env.info('ZoneCommand:verifyBuildValid - stopping build, mode is '..self.mode..' but mission is not supply') + self.currentBuild = nil + end + end + + if self.currentBuild and (self.currentBuild.product.type == 'defense' or self.currentBuild.product.type == 'mission') then + for i,v in ipairs(self.upgrades[self.currentBuild.side]) do + for i2,v2 in ipairs(v.products) do + if v2.name == self.currentBuild.product.name then + local g = Group.getByName(v.name) + if not g then g = StaticObject.getByName(v.name) end + + if not g then + env.info('ZoneCommand:verifyBuildValid - stopping build, required upgrade no longer exists') + self.currentBuild = nil + break + end + end + end + + if not self.currentBuild then + break + end + end + end + end + + if self.currentMissionBuild then + if self.side == 0 then + self.currentMissionBuild = nil + env.info('ZoneCommand:verifyBuildValid - stopping mission build, zone is neutral') + end + + if (self.mode == 'export' and not self.keepActive) or self.mode == 'supply' then + env.info('ZoneCommand:verifyBuildValid - stopping mission build, mode is '..self.mode..'') + self.currentMissionBuild = nil + end + + if self.currentMissionBuild and self.currentMissionBuild.product.type == 'mission' then + for i,v in ipairs(self.upgrades[self.currentMissionBuild.side]) do + for i2,v2 in ipairs(v.products) do + if v2.name == self.currentMissionBuild.product.name then + local g = Group.getByName(v.name) + if not g then g = StaticObject.getByName(v.name) end + + if not g then + env.info('ZoneCommand:verifyBuildValid - stopping mission build, required upgrade no longer exists') + self.currentMissionBuild = nil + break + end + end + end + + if not self.currentMissionBuild then + break + end + end + end + end + end + + function ZoneCommand:chooseBuild() + local treshhold = self.spendTreshold + --local treshhold = 0 + if self.side ~= 0 and self.currentBuild == nil then + local canAfford = {} + for _,v in ipairs(self.upgrades[self.side]) do + local u = Group.getByName(v.name) + if not u then u = StaticObject.getByName(v.name) end + + if not u then + table.insert(canAfford, {product = v, reason='upgrade'}) + elseif u ~= nil then + for _,v2 in ipairs(v.products) do + if v2.type == 'mission' then + if self.resource > treshhold and + (v2.missionType == ZoneCommand.missionTypes.supply_air or + v2.missionType == ZoneCommand.missionTypes.supply_convoy or + v2.missionType == ZoneCommand.missionTypes.supply_transfer) then + if self:isMissionValid(v2) and math.random() < ZoneCommand.missionValidChance then + table.insert(canAfford, {product = v2, reason='mission'}) + if v2.bias then + for _=1,v2.bias,1 do + table.insert(canAfford, {product = v2, reason='mission'}) + end + end + end + end + elseif v2.type=='defense' and self.mode ~='export' and self.mode ~='supply' and v2.cost > 0 then + local g = Group.getByName(v2.name) + if not g then + table.insert(canAfford, {product = v2, reason='defense'}) + elseif g:getSize() < (g:getInitialSize()*math.random(40,100)/100) then + table.insert(canAfford, {product = v2, reason='repair'}) + end + end + end + end + end + + if #canAfford > 0 then + local choice = math.random(1, #canAfford) + + if canAfford[choice] then + local p = canAfford[choice] + if p.reason == 'repair' then + self:queueBuild(p.product, self.side, true) + else + self:queueBuild(p.product, self.side) + end + end + end + end + + if self.side ~= 0 and self.currentMissionBuild == nil then + local canMission = {} + for _,v in ipairs(self.upgrades[self.side]) do + local u = Group.getByName(v.name) + if not u then u = StaticObject.getByName(v.name) end + if u ~= nil then + for _,v2 in ipairs(v.products) do + if v2.type == 'mission' then + if v2.missionType ~= ZoneCommand.missionTypes.supply_air and + v2.missionType ~= ZoneCommand.missionTypes.supply_convoy and + v2.missionType ~= ZoneCommand.missionTypes.supply_transfer then + if self:isMissionValid(v2) and math.random() < ZoneCommand.missionValidChance then + table.insert(canMission, {product = v2, reason='mission'}) + if v2.bias then + for _=1,v2.bias,1 do + table.insert(canMission, {product = v2, reason='mission'}) + end + end + end + end + end + end + end + end + + if #canMission > 0 then + local choice = math.random(1, #canMission) + + if canMission[choice] then + local p = canMission[choice] + self:queueBuild(p.product, self.side) + end + end + end + end + + function ZoneCommand:progressBuild() + if self.currentBuild and self.currentBuild.side ~= self.side then + env.info('ZoneCommand:progressBuild '..self.name..' - stopping build, zone changed owner') + self.currentBuild = nil + end + + if self.currentMissionBuild and self.currentMissionBuild.side ~= self.side then + env.info('ZoneCommand:progressBuild '..self.name..' - stopping mission build, zone changed owner') + self.currentMissionBuild = nil + end + + if self.currentBuild then + if self.currentBuild.product.type == 'mission' and not self:isMissionValid(self.currentBuild.product) then + env.info('ZoneCommand:progressBuild '..self.name..' - stopping build, mission no longer valid') + self.currentBuild = nil + else + local cost = self.currentBuild.product.cost + if self.currentBuild.isRepair then + cost = math.floor(self.currentBuild.product.cost/2) + end + + if self.currentBuild.progress < cost then + if self.currentBuild.isRepair and not Group.getByName(self.currentBuild.product.name) then + env.info('ZoneCommand:progressBuild '..self.name..' - stopping build, group to repair no longer exists') + self.currentBuild = nil + else + if self.currentBuild.isRepair then + local gr = Group.getByName(self.currentBuild.product.name) + if gr and self.currentBuild.unitcount and gr:getSize() < self.currentBuild.unitcount then + env.info('ZoneCommand:progressBuild '..self.name..' - restarting build, group to repair has casualties') + self.currentBuild.unitcount = gr:getSize() + self:addResource(self.currentBuild.progress) + self.currentBuild.progress = 0 + end + end + + local step = math.floor(ZoneCommand.buildSpeed * self.boostScale) + if self.currentBuild.product.type == ZoneCommand.productTypes.mission then + if self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_air or + self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_convoy or + self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_transfer then + step = math.floor(ZoneCommand.supplyBuildSpeed * self.boostScale) + + if self.currentBuild.product.missionType == ZoneCommand.missionTypes.supply_transfer then + step = math.floor(step*2) + end + end + end + + if step > self.resource then step = 1 end + if step <= self.resource then + self:removeResource(step) + self.currentBuild.progress = self.currentBuild.progress + step + + if self.extraBuildResources > 0 then + local extrastep = step + if self.extraBuildResources < extrastep then + extrastep = self.extraBuildResources + end + + self.extraBuildResources = math.max(self.extraBuildResources - extrastep, 0) + self.currentBuild.progress = self.currentBuild.progress + extrastep + + env.info('ZoneCommand:progressBuild - '..self.name..' consumed '..extrastep..' extra resources, remaining '..self.extraBuildResources) + end + end + end + else + if self.currentBuild.product.type == 'mission' then + if self:isMissionValid(self.currentBuild.product) then + self:activateMission(self.currentBuild.product) + else + self:addResource(self.currentBuild.product.cost) + end + elseif self.currentBuild.product.type == 'defense' or self.currentBuild.product.type=='upgrade' then + if self.currentBuild.isRepair then + if Group.getByName(self.currentBuild.product.name) then + self.zone:spawnGroup(self.currentBuild.product, self.spawnSurface) + end + else + self.zone:spawnGroup(self.currentBuild.product, self.spawnSurface) + end + + self.built[self.currentBuild.product.name] = self.currentBuild.product + end + + self.currentBuild = nil + end + end + end + + if self.currentMissionBuild then + if self.currentMissionBuild.product.type == 'mission' and not self:isMissionValid(self.currentMissionBuild.product) then + env.info('ZoneCommand:progressBuild '..self.name..' - stopping build, mission no longer valid') + self.currentMissionBuild = nil + else + local cost = self.currentMissionBuild.product.cost + + if self.currentMissionBuild.progress < cost then + local step = math.floor(ZoneCommand.buildSpeed * self.boostScale) + + if step > self.resource then step = 1 end + + local progress = step*self.missionBuildSpeedReduction + local reducedCost = math.max(1, math.floor(progress)) + if reducedCost <= self.resource then + self:removeResource(reducedCost) + self.currentMissionBuild.progress = self.currentMissionBuild.progress + progress + end + else + if self:isMissionValid(self.currentMissionBuild.product) then + self:activateMission(self.currentMissionBuild.product) + else + self:addResource(self.currentMissionBuild.product.cost) + end + + self.currentMissionBuild = nil + end + end + end + end + + function ZoneCommand:queueBuild(product, side, isRepair, progress) + if product.type ~= ZoneCommand.productTypes.mission or + (product.missionType == ZoneCommand.missionTypes.supply_air or + product.missionType == ZoneCommand.missionTypes.supply_convoy or + product.missionType == ZoneCommand.missionTypes.supply_transfer) then + + local unitcount = nil + if isRepair then + local g = Group.getByName(product.name) + if g then + unitcount = g:getSize() + env.info('ZoneCommand:queueBuild - '..self.name..' '..product.name..' has '..unitcount..' units') + end + end + + self.currentBuild = { product = product, progress = (progress or 0), side = side, isRepair = isRepair, unitcount = unitcount} + env.info('ZoneCommand:queueBuild - '..self.name..' chose '..product.name..'('..product.display..') as its build') + else + self.currentMissionBuild = { product = product, progress = (progress or 0), side = side} + env.info('ZoneCommand:queueBuild - '..self.name..' chose '..product.name..'('..product.display..') as its mission build') + end + end + + function ZoneCommand:reserveMission(product) + self.reservedMissions[product.name] = product + end + + function ZoneCommand:unReserveMission(product) + self.reservedMissions[product.name] = nil + end + + function ZoneCommand:isMissionValid(product) + if Group.getByName(product.name) then return false end + + if self.reservedMissions[product.name] then + return false + end + + if product.missionType == ZoneCommand.missionTypes.supply_convoy then + if self.distToFront == nil then return false end + + for _,tgt in pairs(self.neighbours) do + if self:isSupplyMissionValid(product, tgt) then + return true + end + end + elseif product.missionType == ZoneCommand.missionTypes.supply_transfer then + if self.distToFront == nil then return false end + for _,tgt in pairs(self.neighbours) do + if self:isSupplyTransferMissionValid(product, tgt) then + return true + end + end + elseif product.missionType == ZoneCommand.missionTypes.supply_air then + if self.distToFront == nil then return false end + + for _,tgt in pairs(self.neighbours) do + if self:isSupplyMissionValid(product, tgt) then + return true + else + for _,subtgt in pairs(tgt.neighbours) do + if subtgt.name ~= self.name and self:isSupplyMissionValid(product, subtgt) then + local dist = mist.utils.get2DDist(self.zone.point, subtgt.zone.point) + if dist < 50000 then + return true + end + end + end + end + end + elseif product.missionType == ZoneCommand.missionTypes.assault then + if self.mode ~= ZoneCommand.modes.normal then return false end + for _,tgt in pairs(self.neighbours) do + if self:isAssaultMissionValid(product, tgt) then + return true + end + end + elseif product.missionType == ZoneCommand.missionTypes.cas then + if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end + + for _,tgt in pairs(ZoneCommand.getAllZones()) do + if self:isCasMissionValid(product, tgt) then + return true + end + end + elseif product.missionType == ZoneCommand.missionTypes.cas_helo then + if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end + + for _,tgt in pairs(self.neighbours) do + if self:isCasMissionValid(product, tgt) then + return true + end + end + elseif product.missionType == ZoneCommand.missionTypes.strike then + if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end + + for _,tgt in pairs(ZoneCommand.getAllZones()) do + if self:isStrikeMissionValid(product, tgt) then + return true + end + end + elseif product.missionType == ZoneCommand.missionTypes.sead then + if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end + + for _,tgt in pairs(ZoneCommand.getAllZones()) do + if self:isSeadMissionValid(product, tgt) then + return true + end + end + elseif product.missionType == ZoneCommand.missionTypes.patrol then + if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end + + for _,tgt in pairs(ZoneCommand.getAllZones()) do + if self:isPatrolMissionValid(product, tgt) then + return true + end + end + elseif product.missionType == ZoneCommand.missionTypes.bai then + if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end + + for _,tgt in pairs(DependencyManager.get("GroupMonitor").groups) do + if self:isBaiMissionValid(product, tgt) then + return true + end + end + elseif product.missionType == ZoneCommand.missionTypes.awacs then + if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end + for _,tgt in pairs(ZoneCommand.getAllZones()) do + if self:isAwacsMissionValid(product, tgt) then + return true + end + end + elseif product.missionType == ZoneCommand.missionTypes.tanker then + if self.mode ~= ZoneCommand.modes.normal and not self.keepActive then return false end + if not self.distToFront or self.distToFront == 0 then return false end + for _,tgt in pairs(ZoneCommand.getAllZones()) do + if self:isTankerMissionValid(product, tgt) then + return true + end + end + end + end + + function ZoneCommand:activateMission(product) + if product.missionType == ZoneCommand.missionTypes.supply_convoy then + self:activateSupplyConvoyMission(product) + elseif product.missionType == ZoneCommand.missionTypes.assault then + self:activateAssaultMission(product) + elseif product.missionType == ZoneCommand.missionTypes.supply_air then + self:activateAirSupplyMission(product) + elseif product.missionType == ZoneCommand.missionTypes.supply_transfer then + self:activateSupplyTransferMission(product) + elseif product.missionType == ZoneCommand.missionTypes.cas then + self:activateCasMission(product) + elseif product.missionType == ZoneCommand.missionTypes.cas_helo then + self:activateCasMission(product, true) + elseif product.missionType == ZoneCommand.missionTypes.strike then + self:activateStrikeMission(product) + elseif product.missionType == ZoneCommand.missionTypes.sead then + self:activateSeadMission(product) + elseif product.missionType == ZoneCommand.missionTypes.patrol then + self:activatePatrolMission(product) + elseif product.missionType == ZoneCommand.missionTypes.bai then + self:activateBaiMission(product) + elseif product.missionType == ZoneCommand.missionTypes.awacs then + self:activateAwacsMission(product) + elseif product.missionType == ZoneCommand.missionTypes.tanker then + self:activateTankerMission(product) + end + + env.info('ZoneCommand:activateMission - '..self.name..' activating mission '..product.name..'('..product.display..')') + end + + function ZoneCommand:reActivateMission(savedData) + local product = self:getProductByName(savedData.productName) + + if product.missionType == ZoneCommand.missionTypes.supply_convoy then + self:reActivateSupplyConvoyMission(product, savedData) + elseif product.missionType == ZoneCommand.missionTypes.assault then + self:reActivateAssaultMission(product, savedData) + elseif product.missionType == ZoneCommand.missionTypes.supply_air then + self:reActivateAirSupplyMission(product, savedData) + elseif product.missionType == ZoneCommand.missionTypes.supply_transfer then + self:reActivateSupplyTransferMission(product, savedData) + elseif product.missionType == ZoneCommand.missionTypes.cas then + self:reActivateCasMission(product, nil, savedData) + elseif product.missionType == ZoneCommand.missionTypes.cas_helo then + self:reActivateCasMission(product, true, savedData) + elseif product.missionType == ZoneCommand.missionTypes.strike then + self:reActivateStrikeMission(product, savedData) + elseif product.missionType == ZoneCommand.missionTypes.sead then + self:reActivateSeadMission(product, savedData) + elseif product.missionType == ZoneCommand.missionTypes.patrol then + self:reActivatePatrolMission(product, savedData) + elseif product.missionType == ZoneCommand.missionTypes.bai then + self:reActivateBaiMission(product, savedData) + elseif product.missionType == ZoneCommand.missionTypes.awacs then + self:reActivateAwacsMission(product, savedData) + elseif product.missionType == ZoneCommand.missionTypes.tanker then + self:reActivateTankerMission(product, savedData) + end + + env.info('ZoneCommand:reActivateMission - '..self.name..' reactivating mission '..product.name..'('..product.display..')') + end + + local function getDefaultPos(savedData, isAir) + local action = 'Off Road' + local speed = 0 + if isAir then + action = 'Turning Point' + speed = 250 + end + + local vars = { + groupName = savedData.productName, + point = savedData.position, + action = 'respawn', + heading = savedData.heading, + initTasks = false, + route = { + [1] = { + alt = savedData.position.y, + type = 'Turning Point', + action = action, + alt_type = 'BARO', + x = savedData.position.x, + y = savedData.position.z, + speed = speed + } + } + } + + return vars + end + + local function teleportToPos(groupName, pos) + if pos.y == nil then + pos.y = land.getHeight({ x = pos.x, y = pos.z }) + end + + local vars = { + groupName = groupName, + point = pos, + action = 'respawn', + initTasks = false + } + + mist.teleportToPoint(vars) + end + + function ZoneCommand:reActivateSupplyConvoyMission(product, savedData) + local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName) + + local supplyPoint = trigger.misc.getZone(zone.name..'-sp') + if not supplyPoint then + supplyPoint = trigger.misc.getZone(zone.name) + end + if supplyPoint then + mist.teleportToPoint(getDefaultPos(savedData, false)) + DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData) + + product.lastMission = {zoneName = zone.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.name) + TaskExtensions.moveOnRoadToPoint(gr, param.point) + end, {name=product.name, point={ x=supplyPoint.point.x, y = supplyPoint.point.z}}, timer.getTime()+1) + end + end + + function ZoneCommand:reActivateAssaultMission(product, savedData) + local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName) + + local supplyPoint = trigger.misc.getZone(zone.name..'-sp') + if not supplyPoint then + supplyPoint = trigger.misc.getZone(zone.name) + end + if supplyPoint then + mist.teleportToPoint(getDefaultPos(savedData, false)) + DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData) + + local tgtPoint = trigger.misc.getZone(zone.name) + + if tgtPoint then + product.lastMission = {zoneName = zone.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.name) + TaskExtensions.moveOnRoadToPointAndAssault(gr, param.point, param.targets) + end, {name=product.name, point={ x=tgtPoint.point.x, y = tgtPoint.point.z}, targets=zone.built}, timer.getTime()+1) + end + end + end + + function ZoneCommand:reActivateAirSupplyMission(product, savedData) + local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName) + + mist.teleportToPoint(getDefaultPos(savedData, true)) + DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData) + + local supplyPoint = trigger.misc.getZone(zone.name..'-hsp') + if not supplyPoint then + supplyPoint = trigger.misc.getZone(zone.name) + end + + if supplyPoint then + product.lastMission = {zoneName = zone.name} + local alt = DependencyManager.get("ConnectionManager"):getHeliAlt(self.name, zone.name) + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.name) + TaskExtensions.landAtPoint(gr, param.point, param.alt) + end, {name=product.name, point={ x=supplyPoint.point.x, y = supplyPoint.point.z}, alt = alt}, timer.getTime()+1) + end + end + + function ZoneCommand:reActivateSupplyTransferMission(product, savedData) + -- not needed + end + + function ZoneCommand:reActivateCasMission(product, isHelo, savedData) + local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName) + + mist.teleportToPoint(getDefaultPos(savedData, true)) + DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData) + + local homePos = trigger.misc.getZone(savedData.homeName).point + + if zone then + product.lastMission = {zoneName = zone.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + if param.helo then + TaskExtensions.executeHeloCasMission(gr, param.targets, param.prod.expend, param.prod.altitude, {homePos = homePos}) + else + TaskExtensions.executeCasMission(gr, param.targets, param.prod.expend, param.prod.altitude, {homePos = homePos}) + end + end, {prod=product, targets=zone.built, helo = isHelo, homePos = homePos}, timer.getTime()+1) + end + end + + function ZoneCommand:reActivateStrikeMission(product, savedData) + local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName) + + mist.teleportToPoint(getDefaultPos(savedData, true)) + + DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData) + + local homePos = trigger.misc.getZone(savedData.homeName).point + + if zone then + product.lastMission = {zoneName = zone.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + TaskExtensions.executeStrikeMission(gr, param.targets, param.prod.expend, param.prod.altitude, {homePos = homePos}) + end, {prod=product, targets=zone.built, homePos = homePos}, timer.getTime()+1) + end + end + + function ZoneCommand:reActivateSeadMission(product, savedData) + local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName) + + mist.teleportToPoint(getDefaultPos(savedData, true)) + DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData) + + local homePos = trigger.misc.getZone(savedData.homeName).point + + if zone then + product.lastMission = {zoneName = zone.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + TaskExtensions.executeSeadMission(gr, param.targets, param.prod.expend, param.prod.altitude, {homePos = homePos}) + end, {prod=product, targets=zone.built, homePos = homePos}, timer.getTime()+1) + end + end + + function ZoneCommand:reActivatePatrolMission(product, savedData) + + local zn1 = ZoneCommand.getZoneByName(savedData.lastMission.zone1name) + + mist.teleportToPoint(getDefaultPos(savedData, true)) + DependencyManager.get("GroupMonitor"):registerGroup(product, zn1, self, savedData) + + local homePos = trigger.misc.getZone(savedData.homeName).point + + if zn1 then + product.lastMission = {zone1name = zn1.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + + local point = trigger.misc.getZone(param.zone1.name).point + + TaskExtensions.executePatrolMission(gr, point, param.prod.altitude, param.prod.range, {homePos = param.homePos}) + end, {prod=product, zone1 = zn1, homePos = homePos}, timer.getTime()+1) + end + end + + function ZoneCommand:reActivateBaiMission(product, savedData) + local targets = {} + local hasTarget = false + for _,tgt in pairs(DependencyManager.get("GroupMonitor").groups) do + if self:isBaiMissionValid(product, tgt) then + targets[tgt.product.name] = tgt.product + hasTarget = true + end + end + + local homePos = trigger.misc.getZone(savedData.homeName).point + + if hasTarget then + mist.teleportToPoint(getDefaultPos(savedData, true)) + DependencyManager.get("GroupMonitor"):registerGroup(product, nil, self, savedData) + + product.lastMission = { active = true } + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + TaskExtensions.executeBaiMission(gr, param.targets, param.prod.expend, param.prod.altitude, {homePos = param.homePos}) + end, {prod=product, targets=targets, homePos = homePos}, timer.getTime()+1) + end + end + + function ZoneCommand:reActivateAwacsMission(product, savedData) + + local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName) + local homePos = trigger.misc.getZone(savedData.homeName).point + + mist.teleportToPoint(getDefaultPos(savedData, true)) + DependencyManager.get("GroupMonitor"):registerGroup(product, nil, self, savedData) + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + if gr then + local un = gr:getUnit(1) + if un then + local callsign = un:getCallsign() + RadioFrequencyTracker.registerRadio(param.prod.name, '[AWACS] '..callsign, param.prod.freq..' AM') + end + + local point = trigger.misc.getZone(param.target.name).point + product.lastMission = { zoneName = param.target.name } + TaskExtensions.executeAwacsMission(gr, point, param.prod.altitude, param.prod.freq, {homePos = param.homePos}) + end + end, {prod=product, target=zone, homePos = homePos}, timer.getTime()+1) + end + + function ZoneCommand:reActivateTankerMission(product, savedData) + + local zone = ZoneCommand.getZoneByName(savedData.lastMission.zoneName) + + local homePos = trigger.misc.getZone(savedData.homeName).point + mist.teleportToPoint(getDefaultPos(savedData, true)) + DependencyManager.get("GroupMonitor"):registerGroup(product, zone, self, savedData) + + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + if gr then + local un = gr:getUnit(1) + if un then + local callsign = un:getCallsign() + RadioFrequencyTracker.registerRadio(param.prod.name, '[Tanker('..param.prod.variant..')] '..callsign, param.prod.freq..' AM | TCN '..param.prod.tacan..'X') + end + + local point = trigger.misc.getZone(param.target.name).point + product.lastMission = { zoneName = param.target.name } + TaskExtensions.executeTankerMission(gr, point, param.prod.altitude, param.prod.freq, param.prod.tacan, {homePos = param.homePos}) + end + end, {prod=product, target=zone, homePos = homePos}, timer.getTime()+1) + end + + function ZoneCommand:isBaiMissionValid(product, tgtgroup) + if product.side == tgtgroup.product.side then return false end + if tgtgroup.product.type ~= ZoneCommand.productTypes.mission then return false end + if tgtgroup.product.missionType == ZoneCommand.missionTypes.assault then return true end + if tgtgroup.product.missionType == ZoneCommand.missionTypes.supply_convoy then return true end + end + + function ZoneCommand:activateBaiMission(product) + --{name = product.name, lastStateTime = timer.getAbsTime(), product = product, target = target} + local targets = {} + local hasTarget = false + for _,tgt in pairs(DependencyManager.get("GroupMonitor").groups) do + if self:isBaiMissionValid(product, tgt) then + targets[tgt.product.name] = tgt.product + hasTarget = true + end + end + + if hasTarget then + local og = Utils.getOriginalGroup(product.name) + if og then + teleportToPos(product.name, {x=og.x, z=og.y}) + env.info("ZoneCommand - activateBaiMission teleporting to OG pos") + else + mist.respawnGroup(product.name, true) + env.info("ZoneCommand - activateBaiMission fallback to respawnGroup") + end + + DependencyManager.get("GroupMonitor"):registerGroup(product, nil, self) + + product.lastMission = { active = true } + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + TaskExtensions.executeBaiMission(gr, param.targets, param.prod.expend, param.prod.altitude) + end, {prod=product, targets=targets}, timer.getTime()+1) + + env.info("ZoneCommand - "..product.name.." targeting convoys") + end + end + + local function prioritizeSupplyTargets(a,b) + --if a:criticalOnSupplies() and not b:criticalOnSupplies() then return true end + --if b:criticalOnSupplies() and not a:criticalOnSupplies() then return false end + + if a.distToFront~=nil and b.distToFront == nil then + return true + elseif a.distToFront == nil and b.distToFront ~= nil then + return false + elseif a.distToFront == b.distToFront then + return a.resource < b.resource + else + return a.distToFront 1 and target.distToFront > 1 then return false end -- skip regular missions if not close to front + + if self.mode == 'normal' and self.distToFront == 0 and target.distToFront == 0 then + return target:needsSupplies(product.cost*0.5) + end + + if target:needsSupplies(product.cost*0.5) and target.distToFront < self.distToFront then + return true + elseif target:criticalOnSupplies() and self.distToFront>=target.distToFront then + return true + end + + if target.mode == 'normal' and target:needsSupplies(product.cost*0.5) then + return true + end + end + end + + function ZoneCommand:activateSupplyConvoyMission(product) + local tgtzones = {} + for _,v in pairs(self.neighbours) do + if (v.side == 0 or v.side==product.side) then + table.insert(tgtzones, v) + end + end + + if #tgtzones == 0 then + env.info('ZoneCommand:activateSupplyConvoyMission - '..self.name..' no valid tgtzones') + return + end + + table.sort(tgtzones, prioritizeSupplyTargets) + + if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then + local prioZone = BattlefieldManager.priorityZones[self.side] + if prioZone.side == 0 and self.neighbours[prioZone.name] and self:isSupplyMissionValid(product, prioZone) then + tgtzones = { prioZone } + end + end + + for i,v in ipairs(tgtzones) do + if self:isSupplyMissionValid(product, v) then + + local supplyPoint = trigger.misc.getZone(v.name..'-sp') + if not supplyPoint then + supplyPoint = trigger.misc.getZone(v.name) + end + + if supplyPoint then + + local og = Utils.getOriginalGroup(product.name) + if og then + teleportToPos(product.name, {x=og.x, z=og.y}) + env.info("ZoneCommand - activateSupplyConvoyMission teleporting to OG pos") + else + mist.respawnGroup(product.name, true) + env.info("ZoneCommand - activateSupplyConvoyMission fallback to respawnGroup") + end + + DependencyManager.get("GroupMonitor"):registerGroup(product, v, self) + + product.lastMission = {zoneName = v.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.name) + TaskExtensions.moveOnRoadToPoint(gr, param.point) + end, {name=product.name, point={ x=supplyPoint.point.x, y = supplyPoint.point.z}}, timer.getTime()+1) + + env.info("ZoneCommand - "..product.name.." targeting "..v.name) + end + + break + end + end + end + + function ZoneCommand:isAssaultMissionValid(product, target) + + if product.missionType == ZoneCommand.missionTypes.assault then + if DependencyManager.get("ConnectionManager"):isRoadBlocked(self.name, target.name) then + return false + end + end + + if target.side ~= product.side and target.side ~= 0 then + return true + end + end + + function ZoneCommand:activateAssaultMission(product) + local tgtzones = {} + for _,v in pairs(self.neighbours) do + table.insert(tgtzones, {zone = v, rank = math.random()}) + end + + table.sort(tgtzones, function(a,b) return a.rank < b.rank end) + + local sorted = {} + for i,v in ipairs(tgtzones) do + table.insert(sorted, v.zone) + end + tgtzones = sorted + + if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then + local prioZone = BattlefieldManager.priorityZones[self.side] + if self.neighbours[prioZone.name] and self:isAssaultMissionValid(product, prioZone) then + tgtzones = { prioZone } + end + end + + for i,v in ipairs(tgtzones) do + if self:isAssaultMissionValid(product, v) then + + local og = Utils.getOriginalGroup(product.name) + if og then + teleportToPos(product.name, {x=og.x, z=og.y}) + env.info("ZoneCommand - activateAssaultMission teleporting to OG pos") + else + mist.respawnGroup(product.name, true) + env.info("ZoneCommand - activateAssaultMission fallback to respawnGroup") + end + + DependencyManager.get("GroupMonitor"):registerGroup(product, v, self) + + local tgtPoint = trigger.misc.getZone(v.name) + + if tgtPoint then + product.lastMission = {zoneName = v.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.name) + TaskExtensions.moveOnRoadToPointAndAssault(gr, param.point, param.targets) + end, {name=product.name, point={ x=tgtPoint.point.x, y = tgtPoint.point.z}, targets=v.built}, timer.getTime()+1) + + env.info("ZoneCommand - "..product.name.." targeting "..v.name) + end + + break + end + end + end + + function ZoneCommand:isAwacsMissionValid(product, target) + if target.side ~= product.side then return false end + if target.name == self.name then return false end + if not target.distToFront or target.distToFront ~= 4 then return false end + + return true + end + + function ZoneCommand:activateAwacsMission(product) + local tgtzones = {} + for _,v in pairs(ZoneCommand.getAllZones()) do + if self:isAwacsMissionValid(product, v) then + table.insert(tgtzones, v) + end + end + + local choice1 = math.random(1,#tgtzones) + local zn = tgtzones[choice1] + + local og = Utils.getOriginalGroup(product.name) + if og then + teleportToPos(product.name, {x=og.x, z=og.y}) + env.info("ZoneCommand - activateAwacsMission teleporting to OG pos") + else + mist.respawnGroup(product.name, true) + env.info("ZoneCommand - activateAwacsMission fallback to respawnGroup") + end + + DependencyManager.get("GroupMonitor"):registerGroup(product, zn, self) + + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + if gr then + local un = gr:getUnit(1) + if un then + local callsign = un:getCallsign() + RadioFrequencyTracker.registerRadio(param.prod.name, '[AWACS] '..callsign, param.prod.freq..' AM') + end + + local point = trigger.misc.getZone(param.target.name).point + product.lastMission = { zoneName = param.target.name } + TaskExtensions.executeAwacsMission(gr, point, param.prod.altitude, param.prod.freq) + + end + end, {prod=product, target=zn}, timer.getTime()+1) + + env.info("ZoneCommand - "..product.name.." targeting "..zn.name) + end + + function ZoneCommand:isTankerMissionValid(product, target) + if target.side ~= product.side then return false end + if target.name == self.name then return false end + if not target.distToFront or target.distToFront ~= 4 then return false end + + return true + end + + function ZoneCommand:activateTankerMission(product) + + local tgtzones = {} + for _,v in pairs(ZoneCommand.getAllZones()) do + if self:isTankerMissionValid(product, v) then + table.insert(tgtzones, v) + end + end + + local choice1 = math.random(1,#tgtzones) + local zn = tgtzones[choice1] + table.remove(tgtzones, choice1) + + local og = Utils.getOriginalGroup(product.name) + if og then + teleportToPos(product.name, {x=og.x, z=og.y}) + env.info("ZoneCommand - activateTankerMission teleporting to OG pos") + else + mist.respawnGroup(product.name, true) + env.info("ZoneCommand - activateTankerMission fallback to respawnGroup") + end + + DependencyManager.get("GroupMonitor"):registerGroup(product, zn, self) + + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + if gr then + local un = gr:getUnit(1) + if un then + local callsign = un:getCallsign() + RadioFrequencyTracker.registerRadio(param.prod.name, '[Tanker('..param.prod.variant..')] '..callsign, param.prod.freq..' AM | TCN '..param.prod.tacan..'X') + end + + local point = trigger.misc.getZone(param.target.name).point + product.lastMission = { zoneName = param.target.name } + TaskExtensions.executeTankerMission(gr, point, param.prod.altitude, param.prod.freq, param.prod.tacan) + end + end, {prod=product, target=zn}, timer.getTime()+1) + + env.info("ZoneCommand - "..product.name.." targeting "..zn.name) + end + + function ZoneCommand:isPatrolMissionValid(product, target) + --if target.side ~= product.side then return false end + if target.name == self.name then return false end + if not target.distToFront or target.distToFront > 1 then return false end + if target.side ~= product.side and target.side ~= 0 then return false end + local dist = mist.utils.get2DDist(self.zone.point, target.zone.point) + if dist > 150000 then return false end + + return true + end + + function ZoneCommand:activatePatrolMission(product) + local tgtzones = {} + for _,v in pairs(ZoneCommand.getAllZones()) do + if self:isPatrolMissionValid(product, v) then + table.insert(tgtzones, v) + end + end + + local choice1 = math.random(1,#tgtzones) + local zn1 = tgtzones[choice1] + + if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then + local prioZone = BattlefieldManager.priorityZones[self.side] + if self:isPatrolMissionValid(product, prioZone) then + zn1 = prioZone + end + end + + local og = Utils.getOriginalGroup(product.name) + if og then + teleportToPos(product.name, {x=og.x, z=og.y}) + env.info("ZoneCommand - activatePatrolMission teleporting to OG pos") + else + mist.respawnGroup(product.name, true) + env.info("ZoneCommand - activatePatrolMission fallback to respawnGroup") + end + + DependencyManager.get("GroupMonitor"):registerGroup(product, zn1, self) + + if zn1 then + product.lastMission = {zone1name = zn1.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + + local point = trigger.misc.getZone(param.zone1.name).point + + TaskExtensions.executePatrolMission(gr, point, param.prod.altitude, param.prod.range) + end, {prod=product, zone1 = zn1}, timer.getTime()+1) + + env.info("ZoneCommand - "..product.name.." targeting "..zn1.name) + end + end + + function ZoneCommand:isSeadMissionValid(product, target) + if target.side == 0 then return false end + if not target.distToFront or target.distToFront > 1 then return false end + + --if MissionTargetRegistry.isZoneTargeted(target.name) then return false end + + return target:hasEnemySAMRadar(product) + end + + function ZoneCommand:hasEnemySAMRadar(product) + if product.side == 1 then + return self:hasSAMRadarOnSide(2) + elseif product.side == 2 then + return self:hasSAMRadarOnSide(1) + end + end + + function ZoneCommand:hasSAMRadarOnSide(side) + for i,v in pairs(self.built) do + if v.type == ZoneCommand.productTypes.defense and v.side == side then + local gr = Group.getByName(v.name) + if gr then + for _,unit in ipairs(gr:getUnits()) do + if unit:hasAttribute('SAM SR') or unit:hasAttribute('SAM TR') then + return true + end + end + end + end + end + end + + function ZoneCommand:hasRunway() + local zones = self:getRunwayZones() + return #zones > 0 + end + + function ZoneCommand:getRunwayZones() + local runways = {} + for i=1,10,1 do + local name = self.name..'-runway-'..i + local zone = trigger.misc.getZone(name) + if zone then + runways[i] = {name = name, zone = zone} + else + break + end + end + + return runways + end + + function ZoneCommand:getRandomUnitWithAttributeOnSide(attributes, side) + local available = {} + for i,v in pairs(self.built) do + if v.type == ZoneCommand.productTypes.upgrade and v.side == side then + local st = StaticObject.getByName(v.name) + if st then + for _,a in ipairs(attributes) do + if a == "Buildings" and ZoneCommand.staticRegistry[v.name] then -- dcs does not consider all statics buildings so we compensate + table.insert(available, v) + end + end + end + elseif v.type == ZoneCommand.productTypes.defense and v.side == side then + local gr = Group.getByName(v.name) + if gr then + for _,unit in ipairs(gr:getUnits()) do + for _,a in ipairs(attributes) do + if unit:hasAttribute(a) then + table.insert(available, v) + end + end + end + end + end + end + + if #available > 0 then + return available[math.random(1, #available)] + end + end + + function ZoneCommand:hasUnitWithAttributeOnSide(attributes, side, amount) + local count = 0 + + for i,v in pairs(self.built) do + if v.type == ZoneCommand.productTypes.upgrade and v.side == side then + local st = StaticObject.getByName(v.name) + if st and st:isExist() then + for _,a in ipairs(attributes) do + if a == "Buildings" and ZoneCommand.staticRegistry[v.name] then -- dcs does not consider all statics buildings so we compensate + if amount==nil then + return true + else + count = count + 1 + if count >= amount then return true end + end + end + end + end + elseif v.type == ZoneCommand.productTypes.defense and v.side == side then + local gr = Group.getByName(v.name) + if gr then + for _,unit in ipairs(gr:getUnits()) do + if unit:isExist() then + for _,a in ipairs(attributes) do + if unit:hasAttribute(a) then + if amount==nil then + return true + else + count = count + 1 + if count >= amount then return true end + end + end + end + end + end + end + end + end + end + + function ZoneCommand:getUnitCountWithAttributeOnSide(attributes, side) + local count = 0 + + for i,v in pairs(self.built) do + if v.type == ZoneCommand.productTypes.upgrade and v.side == side then + local st = StaticObject.getByName(v.name) + if st then + for _,a in ipairs(attributes) do + if a == "Buildings" and ZoneCommand.staticRegistry[v.name] then + count = count + 1 + break + end + end + end + elseif v.type == ZoneCommand.productTypes.defense and v.side == side then + local gr = Group.getByName(v.name) + if gr then + for _,unit in ipairs(gr:getUnits()) do + for _,a in ipairs(attributes) do + if unit:hasAttribute(a) then + count = count + 1 + break + end + end + end + end + end + end + + return count + end + + function ZoneCommand:activateSeadMission(product) + local tgtzones = {} + for _,v in pairs(ZoneCommand.getAllZones()) do + if self:isSeadMissionValid(product, v) then + table.insert(tgtzones, v) + end + end + + local choice = math.random(1,#tgtzones) + local target = tgtzones[choice] + + if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then + local prioZone = BattlefieldManager.priorityZones[self.side] + if self:isSeadMissionValid(product, prioZone) then + target = prioZone + end + end + + local og = Utils.getOriginalGroup(product.name) + if og then + teleportToPos(product.name, {x=og.x, z=og.y}) + env.info("ZoneCommand - activateSeadMission teleporting to OG pos") + else + mist.respawnGroup(product.name, true) + env.info("ZoneCommand - activateSeadMission fallback to respawnGroup") + end + + DependencyManager.get("GroupMonitor"):registerGroup(product, target, self) + + if target then + product.lastMission = {zoneName = target.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + TaskExtensions.executeSeadMission(gr, param.targets, param.prod.expend, param.prod.altitude) + end, {prod=product, targets=target.built}, timer.getTime()+1) + + env.info("ZoneCommand - "..product.name.." targeting "..target.name) + end + end + + function ZoneCommand:isStrikeMissionValid(product, target) + if target.side == 0 then return false end + if target.side == product.side then return false end + if not target.distToFront or target.distToFront > 0 then return false end + + if target:hasEnemySAMRadar(product) then return false end + + --if MissionTargetRegistry.isZoneTargeted(target.name) then return false end + + for i,v in pairs(target.built) do + if v.type == ZoneCommand.productTypes.upgrade and v.side ~= product.side then + return true + end + end + end + + function ZoneCommand:activateStrikeMission(product) + local tgtzones = {} + for _,v in pairs(ZoneCommand.getAllZones()) do + if self:isStrikeMissionValid(product, v) then + table.insert(tgtzones, v) + end + end + + local choice = math.random(1,#tgtzones) + local target = tgtzones[choice] + + if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then + local prioZone = BattlefieldManager.priorityZones[self.side] + if self:isStrikeMissionValid(product, prioZone) then + target = prioZone + end + end + + local og = Utils.getOriginalGroup(product.name) + if og then + teleportToPos(product.name, {x=og.x, z=og.y}) + env.info("ZoneCommand - activateStrikeMission teleporting to OG pos") + else + mist.respawnGroup(product.name, true) + env.info("ZoneCommand - activateStrikeMission fallback to respawnGroup") + end + + DependencyManager.get("GroupMonitor"):registerGroup(product, target, self) + + if target then + product.lastMission = {zoneName = target.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + TaskExtensions.executeStrikeMission(gr, param.targets, param.prod.expend, param.prod.altitude) + end, {prod=product, targets=target.built}, timer.getTime()+1) + + env.info("ZoneCommand - "..product.name.." targeting "..target.name) + end + end + + function ZoneCommand:isCasMissionValid(product, target) + if target.side == product.side then return false end + if not target.distToFront or target.distToFront > 0 then return false end + + if target:hasEnemySAMRadar(product) then return false end + + --if MissionTargetRegistry.isZoneTargeted(target.name) then return false end + + for i,v in pairs(target.built) do + if v.type == ZoneCommand.productTypes.defense and v.side ~= product.side then + return true + end + end + end + + function ZoneCommand:activateCasMission(product, ishelo) + local viablezones = {} + if ishelo then + viablezones = self.neighbours + else + viablezones = ZoneCommand.getAllZones() + end + + local tgtzones = {} + for _,v in pairs(viablezones) do + if self:isCasMissionValid(product, v) then + table.insert(tgtzones, v) + end + end + + local choice = math.random(1,#tgtzones) + local target = tgtzones[choice] + + if BattlefieldManager and BattlefieldManager.priorityZones[self.side] then + local prioZone = BattlefieldManager.priorityZones[self.side] + if viablezones[prioZone.name] and self:isCasMissionValid(product, prioZone) then + target = prioZone + end + end + + local og = Utils.getOriginalGroup(product.name) + if og then + teleportToPos(product.name, {x=og.x, z=og.y}) + env.info("ZoneCommand - activateCasMission teleporting to OG pos") + else + mist.respawnGroup(product.name, true) + env.info("ZoneCommand - activateCasMission fallback to respawnGroup") + end + + DependencyManager.get("GroupMonitor"):registerGroup(product, target, self) + + if target then + product.lastMission = {zoneName = target.name} + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.prod.name) + if param.helo then + TaskExtensions.executeHeloCasMission(gr, param.targets, param.prod.expend, param.prod.altitude) + else + TaskExtensions.executeCasMission(gr, param.targets, param.prod.expend, param.prod.altitude) + end + end, {prod=product, targets=target.built, helo = ishelo}, timer.getTime()+1) + + env.info("ZoneCommand - "..product.name.." targeting "..target.name) + end + end + + function ZoneCommand:defineUpgrades(upgrades) + self.upgrades = upgrades + + for side,sd in ipairs(self.upgrades) do + for _,v in ipairs(sd) do + v.side = side + + local cat = TemplateDB.getData(v.template) + if cat.dataCategory == TemplateDB.type.static then + ZoneCommand.staticRegistry[v.name] = true + end + + for _,v2 in ipairs(v.products) do + v2.side = side + + if v2.type == "mission" then + local gr = Group.getByName(v2.name) + + if not gr then + if v2.missionType ~= ZoneCommand.missionTypes.supply_transfer then + env.info("ZoneCommand - ERROR declared group does not exist in mission: ".. v2.name) + end + else + gr:destroy() + end + end + end + end + end + end + + function ZoneCommand:getProductByName(name) + for i,v in ipairs(self.upgrades) do + for i2,v2 in ipairs(v) do + if v2.name == name then + return v2 + else + for i3,v3 in ipairs(v2.products) do + if v3.name == name then + return v3 + end + end + end + end + end + + return nil + end + + function ZoneCommand:cleanup() + local zn = trigger.misc.getZone(self.name) + local pos = { + x = zn.point.x, + y = land.getHeight({x = zn.point.x, y = zn.point.z}), + z= zn.point.z + } + local radius = zn.radius*2 + world.removeJunk({id = world.VolumeType.SPHERE,params = {point = pos, radius = radius}}) + end +end + + + + +-----------------[[ END OF ZoneCommand.lua ]]----------------- + + + +-----------------[[ BattlefieldManager.lua ]]----------------- + +BattlefieldManager = {} +do + BattlefieldManager.closeOverride = 27780 -- 15nm + BattlefieldManager.farOverride = Config.maxDistFromFront -- default 100nm + BattlefieldManager.boostScale = {[0] = 1.0, [1]=1.0, [2]=1.0} + BattlefieldManager.noRedZones = false + BattlefieldManager.noBlueZones = false + + BattlefieldManager.priorityZones = { + [1] = nil, + [2] = nil + } + + BattlefieldManager.overridePriorityZones = { + [1] = nil, + [2] = nil + } + + function BattlefieldManager:new() + local obj = {} + + setmetatable(obj, self) + self.__index = self + obj:start() + return obj + end + + function BattlefieldManager:start() + timer.scheduleFunction(function(param, time) + local self = param.context + + local zones = ZoneCommand.getAllZones() + local torank = {} + + --reset ranks and define frontline + for name,zone in pairs(zones) do + zone.distToFront = nil + zone.closestEnemyDist = nil + + if zone.neighbours then + for nName, nZone in pairs(zone.neighbours) do + if zone.side ~= nZone.side then + zone.distToFront = 0 + end + end + end + + --set dist to closest enemy + for name2,zone2 in pairs(zones) do + if zone.side ~= zone2.side then + local dist = mist.utils.get2DDist(zone.zone.point, zone2.zone.point) + if not zone.closestEnemyDist or dist < zone.closestEnemyDist then + zone.closestEnemyDist = dist + end + end + end + end + + for name,zone in pairs(zones) do + if zone.distToFront == 0 then + for nName, nZone in pairs(zone.neighbours) do + if nZone.distToFrount == nil then + table.insert(torank, nZone) + end + end + end + end + + -- build ranks of every other zone + while #torank > 0 do + local nexttorank = {} + for _,zone in ipairs(torank) do + if not zone.distToFront then + local minrank = 999 + for nName,nZone in pairs(zone.neighbours) do + if nZone.distToFront then + if nZone.distToFront BattlefieldManager.farOverride and zone.distToFront > 3 then + zone.mode = ZoneCommand.modes.export + else + if zone.mode ~= ZoneCommand.modes.normal then + zone:fullBuild(1.0) + end + zone.mode = ZoneCommand.modes.normal + end + else + if not zone.distToFront or zone.distToFront == 0 or (zone.closestEnemyDist and zone.closestEnemyDist < BattlefieldManager.closeOverride) then + if zone.mode ~= ZoneCommand.modes.normal then + zone:fullBuild(1.0) + end + zone.mode = ZoneCommand.modes.normal + elseif zone.distToFront == 1 then + zone.mode = ZoneCommand.modes.supply + elseif zone.distToFront > 1 then + zone.mode = ZoneCommand.modes.export + end + end + + zone.boostScale = self.boostScale[zone.side] + end + + return time+60 + end, {context = self}, timer.getTime()+1) + + timer.scheduleFunction(function(param, time) + local self = param.context + + local zones = ZoneCommand.getAllZones() + + local noRed = true + local noBlue = true + for name, zone in pairs(zones) do + if zone.side == 1 then + noRed = false + elseif zone.side == 2 then + noBlue = false + end + + if not noRed and not noBlue then + break + end + end + + if noRed then + BattlefieldManager.noRedZones = true + end + + if noBlue then + BattlefieldManager.noBlueZones = true + end + + return time+10 + end, {context = self}, timer.getTime()+1) + + timer.scheduleFunction(function(param, time) + local self = param.context + + local zones = ZoneCommand.getAllZones() + + local frontLineRed = {} + local frontLineBlue = {} + for name, zone in pairs(zones) do + if zone.distToFront == 0 then + if zone.side == 1 then + table.insert(frontLineRed, zone) + elseif zone.side == 2 then + table.insert(frontLineBlue, zone) + else + table.insert(frontLineRed, zone) + table.insert(frontLineBlue, zone) + end + end + end + + if BattlefieldManager.overridePriorityZones[1] and BattlefieldManager.overridePriorityZones[1].ticks > 0 then + BattlefieldManager.priorityZones[1] = BattlefieldManager.overridePriorityZones[1].zone + BattlefieldManager.overridePriorityZones[1].ticks = BattlefieldManager.overridePriorityZones[1].ticks - 1 + else + local redChangeChance = 1 + if BattlefieldManager.priorityZones[1] and BattlefieldManager.priorityZones[1].side ~= 1 then + redChangeChance = 0.1 + end + + if #frontLineBlue > 0 then + if math.random() <= redChangeChance then + BattlefieldManager.priorityZones[1] = frontLineBlue[math.random(1,#frontLineBlue)] + end + else + BattlefieldManager.priorityZones[1] = nil + end + end + + if BattlefieldManager.overridePriorityZones[2] and BattlefieldManager.overridePriorityZones[2].ticks > 0 then + BattlefieldManager.priorityZones[2] = BattlefieldManager.overridePriorityZones[2].zone + BattlefieldManager.overridePriorityZones[2].ticks = BattlefieldManager.overridePriorityZones[2].ticks - 1 + else + local blueChangeChance = 1 + if BattlefieldManager.priorityZones[2] and BattlefieldManager.priorityZones[2].side ~= 2 then + blueChangeChance = 0.1 + end + + if #frontLineRed > 0 then + if math.random() <= blueChangeChance then + BattlefieldManager.priorityZones[2] = frontLineRed[math.random(1,#frontLineRed)] + end + else + BattlefieldManager.priorityZones[2] = nil + end + end + + if BattlefieldManager.priorityZones[1] then + env.info('BattlefieldManager - red priority: '..BattlefieldManager.priorityZones[1].name) + else + env.info('BattlefieldManager - red no priority') + end + + if BattlefieldManager.priorityZones[2] then + env.info('BattlefieldManager - blue priority: '..BattlefieldManager.priorityZones[2].name) + else + env.info('BattlefieldManager - blue no priority') + end + + if BattlefieldManager.overridePriorityZones[1] and BattlefieldManager.overridePriorityZones[1].ticks == 0 then + BattlefieldManager.overridePriorityZones[1] = nil + end + + if BattlefieldManager.overridePriorityZones[2] and BattlefieldManager.overridePriorityZones[2].ticks == 0 then + BattlefieldManager.overridePriorityZones[2] = nil + end + + return time+(60*30) + end, {context = self}, timer.getTime()+10) + + timer.scheduleFunction(function(param, time) + local x = math.random(-50,50) -- the lower limit benefits blue, higher limit benefits red, adjust to increase limit of random boost variance, default (-50,50) + local boostIntensity = Config.randomBoost -- adjusts the intensity of the random boost variance, default value = 0.0004 + local factor = (x*x*x*boostIntensity)/100 -- the farther x is the higher the factor, negative beneifts blue, pozitive benefits red + param.context.boostScale[1] = 1.0+factor + param.context.boostScale[2] = 1.0-factor + + local red = 0 + local blue = 0 + for i,v in pairs(ZoneCommand.getAllZones()) do + if v.side == 1 then + red = red + 1 + elseif v.side == 2 then + blue = blue + 1 + end + + --v:cleanup() + end + + -- push factor towards coalition with less zones (up to 0.5) + local multiplier = Config.lossCompensation -- adjust this to boost losing side production(higher means losing side gains more advantage) (default 1.25) + local total = red + blue + local redp = (0.5-(red/total))*multiplier + local bluep = (0.5-(blue/total))*multiplier + + -- cap factor to avoid increasing difficulty until the end + redp = math.min(redp, 0.15) + bluep = math.max(bluep, -0.15) + + param.context.boostScale[1] = param.context.boostScale[1] + redp + param.context.boostScale[2] = param.context.boostScale[2] + bluep + + --limit to numbers above 0 + param.context.boostScale[1] = math.max(0.01,param.context.boostScale[1]) + param.context.boostScale[2] = math.max(0.01,param.context.boostScale[2]) + + env.info('BattlefieldManager - power red = '..param.context.boostScale[1]) + env.info('BattlefieldManager - power blue = '..param.context.boostScale[2]) + + return time+(60*30) + end, {context = self}, timer.getTime()+1) + end + + function BattlefieldManager.overridePriority(side, zone, ticks) + BattlefieldManager.overridePriorityZones[side] = { zone = zone, ticks = ticks } + BattlefieldManager.priorityZones[side] = zone + + env.info('BattlefieldManager.overridePriority - '..side..' focusing on '..zone.name) + end +end + +-----------------[[ END OF BattlefieldManager.lua ]]----------------- + + + +-----------------[[ Preset.lua ]]----------------- + +Preset = {} +do + function Preset:new(obj) + setmetatable(obj, self) + self.__index = self + return obj + end + + function Preset:extend(new) + return Preset:new(Utils.merge(self, new)) + end +end + +-----------------[[ END OF Preset.lua ]]----------------- + + + +-----------------[[ PlayerTracker.lua ]]----------------- + +PlayerTracker = {} +do + PlayerTracker.savefile = 'player_stats.json' + PlayerTracker.statTypes = { + xp = 'XP', + cmd = "CMD", + survivalBonus = "SB" + } + + PlayerTracker.cmdShopTypes = { + smoke = 'smoke', + prio = 'prio', + jtac = 'jtac', + bribe1 = 'bribe1', + bribe2 = 'bribe2', + artillery = 'artillery', + sabotage1 = 'sabotage1', + } + + PlayerTracker.cmdShopPrices = { + [PlayerTracker.cmdShopTypes.smoke] = 1, + [PlayerTracker.cmdShopTypes.prio] = 10, + [PlayerTracker.cmdShopTypes.jtac] = 20, + [PlayerTracker.cmdShopTypes.bribe1] = 5, + [PlayerTracker.cmdShopTypes.bribe2] = 10, + [PlayerTracker.cmdShopTypes.artillery] = 15, + [PlayerTracker.cmdShopTypes.sabotage1] = 20, + } + + function PlayerTracker:new() + local obj = {} + obj.stats = {} + obj.config = {} + obj.tempStats = {} + obj.groupMenus = {} + obj.groupShopMenus = {} + obj.groupConfigMenus = {} + obj.groupTgtMenus = {} + obj.playerEarningMultiplier = {} + + if lfs then + local dir = lfs.writedir()..'Missions/Saves/' + lfs.mkdir(dir) + PlayerTracker.savefile = dir..PlayerTracker.savefile + env.info('Pretense - Player stats file path: '..PlayerTracker.savefile) + end + + local save = Utils.loadTable(PlayerTracker.savefile) + if save then + obj.stats = save.stats or {} + obj.config = save.config or {} + end + + setmetatable(obj, self) + self.__index = self + + obj:init() + + DependencyManager.register("PlayerTracker", obj) + return obj + end + + function PlayerTracker:init() + local ev = {} + ev.context = self + function ev:onEvent(event) + if not event.initiator then return end + if not event.initiator.getPlayerName then return end + if not event.initiator.getCoalition then return end + + local player = event.initiator:getPlayerName() + if not player then return end + + local blocked = false + if event.id==world.event.S_EVENT_BIRTH then + if event.initiator and Object.getCategory(event.initiator) == Object.Category.UNIT and + (Unit.getCategoryEx(event.initiator) == Unit.Category.AIRPLANE or Unit.getCategoryEx(event.initiator) == Unit.Category.HELICOPTER) then + + local pname = event.initiator:getPlayerName() + if pname then + local gr = event.initiator:getGroup() + if trigger.misc.getUserFlag(gr:getName())==1 then + blocked = true + trigger.action.outTextForGroup(gr:getID(), 'Can not spawn as '..gr:getName()..' in enemy/neutral zone or zone without enough resources',5) + event.initiator:destroy() + + for i,v in pairs(net.get_player_list()) do + if net.get_name(v) == pname then + net.send_chat_to('Can not spawn as '..gr:getName()..' in enemy/neutral zone or zone without enough resources' , v) + net.force_player_slot(v, 0, '') + break + end + end + end + end + end + end + + if event.id == world.event.S_EVENT_BIRTH and not blocked then + -- init stats for player if not exist + if not self.context.stats[player] then + self.context.stats[player] = {} + end + + -- reset temp track for player + self.context.tempStats[player] = nil + + local minutes = 0 + local multiplier = 1.0 + if self.context.stats[player][PlayerTracker.statTypes.survivalBonus] ~= nil then + minutes = self.context.stats[player][PlayerTracker.statTypes.survivalBonus] + multiplier = PlayerTracker.minutesToMultiplier(minutes) + end + + self.context.playerEarningMultiplier[player] = { spawnTime = timer.getAbsTime(), unit = event.initiator, multiplier = multiplier, minutes = minutes } + + local config = self.context:getPlayerConfig(player) + if config.gci_warning_radius then + local gci = DependencyManager.get("GCI") + gci:registerPlayer(player, event.initiator, config.gci_warning_radius, config.gci_metric) + end + end + + if event.id == world.event.S_EVENT_KILL then + local target = event.target + + if not target then return end + if not target.getCoalition then return end + + if target:getCoalition() == event.initiator:getCoalition() then return end + + local xpkey = PlayerTracker.statTypes.xp + local award = PlayerTracker.getXP(target) + + award = math.floor(award * self.context:getPlayerMultiplier(player)) + + local instantxp = math.floor(award*0.25) + local tempxp = award - instantxp + + self.context:addStat(player, instantxp, PlayerTracker.statTypes.xp) + local msg = '[XP] '..self.context.stats[player][xpkey]..' (+'..instantxp..')' + env.info("PlayerTracker.kill - "..player..' awarded '..tostring(instantxp)..' xp') + + self.context:addTempStat(player, tempxp, PlayerTracker.statTypes.xp) + msg = msg..'\n+'..tempxp..' XP (unclaimed)' + env.info("PlayerTracker.kill - "..player..' awarded '..tostring(tempxp)..' xp (unclaimed)') + + trigger.action.outTextForUnit(event.initiator:getID(), msg, 5) + end + + if event.id==world.event.S_EVENT_EJECTION then + self.context.stats[player] = self.context.stats[player] or {} + local ts = self.context.tempStats[player] + if ts then + local un = event.initiator + local key = PlayerTracker.statTypes.xp + local xp = self.context.tempStats[player][key] + if xp then + xp = xp * self.context:getPlayerMultiplier(player) + trigger.action.outTextForUnit(un:getID(), 'Ejection. 30\% XP claimed', 5) + self.context:addStat(player, math.floor(xp*0.3), PlayerTracker.statTypes.xp) + trigger.action.outTextForUnit(un:getID(), '[XP] '..self.context.stats[player][key]..' (+'..math.floor(xp*0.3)..')', 5) + end + + self.context.tempStats[player] = nil + end + end + + if event.id==world.event.S_EVENT_TAKEOFF then + local un = event.initiator + env.info('PlayerTracker - '..player..' took off in '..tostring(un:getID())..' '..un:getName()) + if self.context.stats[player][PlayerTracker.statTypes.survivalBonus] ~= nil then + self.context.stats[player][PlayerTracker.statTypes.survivalBonus] = nil + trigger.action.outTextForUnit(un:getID(), 'Taken off, survival bonus no longer secure.', 10) + end + + local zn = CarrierCommand.getCarrierOfUnit(un:getName()) + if zn then + zn:removeResource(Config.carrierSpawnCost) + else + zn = ZoneCommand.getZoneOfUnit(un:getName()) + if zn then + zn:removeResource(Config.zoneSpawnCost) + end + end + end + + if event.id==world.event.S_EVENT_ENGINE_SHUTDOWN then + local un = event.initiator + local zn = ZoneCommand.getZoneOfUnit(un:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(un:getName()) + end + if un and un:isExist() and zn and zn.side == un:getCoalition() then + env.info('PlayerTracker - '..player..' has shut down engine of '..tostring(un:getID())..' '..un:getName()..' at '..zn.name) + self.context.stats[player][PlayerTracker.statTypes.survivalBonus] = self.context:getPlayerMinutes(player) + self.context:save() + trigger.action.outTextForUnit(un:getID(), 'Engines shut down. Survival bonus secured.', 10) + env.info('PlayerTracker - '..player..' secured survival bonus of '..self.context.stats[player][PlayerTracker.statTypes.survivalBonus]..' minutes') + end + end + + if event.id==world.event.S_EVENT_LAND then + self.context:validateLanding(event.initiator, player) + end + end + + world.addEventHandler(ev) + self:periodicSave() + self:menuSetup() + + timer.scheduleFunction(function(params, time) + local players = params.context.playerEarningMultiplier + for i,v in pairs(players) do + if v.unit.isExist and v.unit:isExist() then + if v.multiplier < 5.0 and v.unit and v.unit:isExist() and Utils.isInAir(v.unit) then + v.minutes = v.minutes + 1 + v.multiplier = PlayerTracker.minutesToMultiplier(v.minutes) + end + end + end + + return time+60 + end, {context = self}, timer.getTime()+60) + end + + function PlayerTracker:validateLanding(unit, player) + local un = unit + local zn = ZoneCommand.getZoneOfUnit(unit:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(unit:getName()) + end + + env.info('PlayerTracker - '..player..' landed in '..tostring(un:getID())..' '..un:getName()) + if un and zn and zn.side == un:getCoalition() then + trigger.action.outTextForUnit(unit:getID(), "Wait 10 seconds to validate landing...", 10) + timer.scheduleFunction(function(param, time) + local un = param.unit + if not un or not un:isExist() then return end + + local player = param.player + local isLanded = Utils.isLanded(un, true) + local zn = ZoneCommand.getZoneOfUnit(un:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(un:getName()) + end + + env.info('PlayerTracker - '..player..' checking if landed: '..tostring(isLanded)) + + if isLanded then + if zn.isCarrier then + zn:addResource(Config.carrierSpawnCost) + else + zn:addResource(Config.zoneSpawnCost) + end + + if param.context.tempStats[player] then + if zn and zn.side == un:getCoalition() then + param.context.stats[player] = param.context.stats[player] or {} + + trigger.action.outTextForUnit(un:getID(), 'Rewards claimed', 5) + for _,key in pairs(PlayerTracker.statTypes) do + local value = param.context.tempStats[player][key] + env.info("PlayerTracker.landing - "..player..' redeeming '..tostring(value)..' '..key) + if value then + param.context:commitTempStat(player, key) + trigger.action.outTextForUnit(un:getID(), key..' +'..value..'', 5) + end + end + + param.context:save() + end + end + end + end, {player = player, unit = unit, context = self}, timer.getTime()+10) + end + end + + function PlayerTracker:addTempStat(player, amount, stattype) + self.tempStats[player] = self.tempStats[player] or {} + self.tempStats[player][stattype] = self.tempStats[player][stattype] or 0 + self.tempStats[player][stattype] = self.tempStats[player][stattype] + amount + end + + function PlayerTracker:addStat(player, amount, stattype) + self.stats[player] = self.stats[player] or {} + self.stats[player][stattype] = self.stats[player][stattype] or 0 + + if stattype == PlayerTracker.statTypes.xp then + local cur = self:getRank(self.stats[player][stattype]) + if cur then + local nxt = self:getRank(self.stats[player][stattype] + amount) + if nxt and cur.rank < nxt.rank then + trigger.action.outText(player..' has leveled up to rank: '..nxt.name, 10) + if nxt.cmdAward and nxt.cmdAward > 0 then + self:addStat(player, nxt.cmdAward, PlayerTracker.statTypes.cmd) + trigger.action.outText(player.." awarded "..nxt.cmdAward.." CMD tokens", 10) + env.info("PlayerTracker.addStat - Awarded "..player.." "..nxt.cmdAward.." CMD tokens for rank up to "..nxt.name) + end + end + end + end + + self.stats[player][stattype] = self.stats[player][stattype] + amount + end + + function PlayerTracker:commitTempStat(player, statkey) + local value = self.tempStats[player][statkey] + if value then + self:addStat(player, value, statkey) + + self.tempStats[player][statkey] = nil + end + end + + function PlayerTracker:addRankRewards(player, unit, isTemp) + local rank = self:getPlayerRank(player) + if not rank then return end + + local cmdChance = rank.cmdChance + if cmdChance > 0 then + + local tkns = 0 + for i=1,rank.cmdTrys,1 do + local die = math.random() + if die <= cmdChance then + tkns = tkns + 1 + end + end + + if tkns > 0 then + if isTemp then + self:addTempStat(player, tkns, PlayerTracker.statTypes.cmd) + else + self:addStat(player, tkns, PlayerTracker.statTypes.cmd) + end + + local msg = "" + if isTemp then + msg = '+'..tkns..' CMD (unclaimed)' + else + msg = '[CMD] '..self.stats[player][PlayerTracker.statTypes.cmd]..' (+'..tkns..')' + end + + trigger.action.outTextForUnit(unit:getID(), msg, 5) + env.info("PlayerTracker.addRankRewards - Awarded "..player.." "..tkns.." CMD tokens with chance "..cmdChance) + end + end + end + + function PlayerTracker.getXP(unit) + local xp = 30 + + if unit:hasAttribute('Planes') then xp = xp + 20 end + if unit:hasAttribute('Helicopters') then xp = xp + 20 end + if unit:hasAttribute('Infantry') then xp = xp + 10 end + if unit:hasAttribute('SAM SR') then xp = xp + 15 end + if unit:hasAttribute('SAM TR') then xp = xp + 15 end + if unit:hasAttribute('IR Guided SAM') then xp = xp + 10 end + if unit:hasAttribute('Ships') then xp = xp + 20 end + if unit:hasAttribute('Buildings') then xp = xp + 30 end + if unit:hasAttribute('Tanks') then xp = xp + 10 end + + return xp + end + + function PlayerTracker:menuSetup() + + MenuRegistry:register(1, function(event, context) + if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + local groupname = event.initiator:getGroup():getName() + + if context.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid]) + context.groupMenus[groupid] = nil + end + + if not context.groupMenus[groupid] then + + local menu = missionCommands.addSubMenuForGroup(groupid, 'Information') + missionCommands.addCommandForGroup(groupid, 'Player', menu, Utils.log(context.showGroupStats), context, groupname) + missionCommands.addCommandForGroup(groupid, 'Frequencies', menu, Utils.log(context.showFrequencies), context, groupname) + missionCommands.addCommandForGroup(groupid, 'Validate Landing', menu, Utils.log(context.validateLandingMenu), context, groupname) + + context.groupMenus[groupid] = menu + end + end + elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + + if context.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid]) + context.groupMenus[groupid] = nil + end + end + end + end, self) + + MenuRegistry:register(5, function(event, context) + if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local rank = context:getPlayerRank(player) + if not rank then return end + + local groupid = event.initiator:getGroup():getID() + local groupname = event.initiator:getGroup():getName() + + if context.groupConfigMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupConfigMenus[groupid]) + context.groupConfigMenus[groupid] = nil + end + + if not context.groupConfigMenus[groupid] then + + local menu = missionCommands.addSubMenuForGroup(groupid, 'Config') + local missionWarningMenu = missionCommands.addSubMenuForGroup(groupid, 'No mission warning', menu) + missionCommands.addCommandForGroup(groupid, 'Activate', missionWarningMenu, Utils.log(context.setNoMissionWarning), context, groupname, true) + missionCommands.addCommandForGroup(groupid, 'Deactivate', missionWarningMenu, Utils.log(context.setNoMissionWarning), context, groupname, false) + + context.groupConfigMenus[groupid] = menu + end + + if context.groupShopMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupShopMenus[groupid]) + context.groupShopMenus[groupid] = nil + end + + if context.groupTgtMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupTgtMenus[groupid]) + context.groupTgtMenus[groupid] = nil + end + + if not context.groupShopMenus[groupid] then + + local menu = missionCommands.addSubMenuForGroup(groupid, 'Command & Control') + missionCommands.addCommandForGroup(groupid, 'Deploy Smoke ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.smoke]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.smoke) + missionCommands.addCommandForGroup(groupid, 'Hack enemy comms ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.bribe1]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.bribe1) + missionCommands.addCommandForGroup(groupid, 'Prioritize zone ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.prio]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.prio) + missionCommands.addCommandForGroup(groupid, 'Bribe enemy officer ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.bribe2]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.bribe2) + missionCommands.addCommandForGroup(groupid, 'Shell zone with artillery ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.artillery]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.artillery) + missionCommands.addCommandForGroup(groupid, 'Sabotage enemy zone ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.sabotage1]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.sabotage1) + + if CommandFunctions.jtac then + missionCommands.addCommandForGroup(groupid, 'Deploy JTAC ['..PlayerTracker.cmdShopPrices[PlayerTracker.cmdShopTypes.jtac]..' CMD]', menu, Utils.log(context.buyCommand), context, groupname, PlayerTracker.cmdShopTypes.jtac) + end + + context.groupShopMenus[groupid] = menu + end + end + elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + + if context.groupShopMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupShopMenus[groupid]) + context.groupShopMenus[groupid] = nil + end + + if context.groupTgtMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupTgtMenus[groupid]) + context.groupTgtMenus[groupid] = nil + end + end + end + end, self) + + DependencyManager.get("MarkerCommands"):addCommand('stats',function(event, _, state) + local unit = nil + if event.initiator then + unit = event.initiator + elseif world.getPlayer() then + unit = world.getPlayer() + end + + if not unit then return false end + + state:showGroupStats(unit:getGroup():getName()) + return true + end, false, self) + + DependencyManager.get("MarkerCommands"):addCommand('freqs',function(event, _, state) + local unit = nil + if event.initiator then + unit = event.initiator + elseif world.getPlayer() then + unit = world.getPlayer() + end + + if not unit then return false end + + state:showFrequencies(unit:getGroup():getName()) + return true + end, false, self) + end + + function PlayerTracker:setNoMissionWarning(groupname, active) + local gr = Group.getByName(groupname) + if gr and gr:getSize()>0 then + local un = gr:getUnit(1) + if un then + local player = un:getPlayerName() + if player then + self:setPlayerConfig(player, "noMissionWarning", active) + end + end + end + end + + function PlayerTracker:buyCommand(groupname, itemType) + local gr = Group.getByName(groupname) + if gr and gr:getSize()>0 then + local un = gr:getUnit(1) + if un then + local player = un:getPlayerName() + local cost = PlayerTracker.cmdShopPrices[itemType] + local cmdTokens = self.stats[player][PlayerTracker.statTypes.cmd] + + if cmdTokens and cost <= cmdTokens then + local canPurchase = true + + if self.groupTgtMenus[gr:getID()] then + missionCommands.removeItemForGroup(gr:getID(), self.groupTgtMenus[gr:getID()]) + self.groupTgtMenus[gr:getID()] = nil + end + + if itemType == PlayerTracker.cmdShopTypes.smoke then + + self.groupTgtMenus[gr:getID()] = MenuRegistry.showTargetZoneMenu(gr:getID(), "Smoke Marker target", function(params) + CommandFunctions.smokeTargets(params.zone, 5) + trigger.action.outTextForGroup(params.groupid, "Targets marked at "..params.zone.name.." with red smoke", 5) + end, 1, 1, nil, nil, true) + + if self.groupTgtMenus[gr:getID()] then + trigger.action.outTextForGroup(gr:getID(), "Select target from radio menu",10) + else + trigger.action.outTextForGroup(gr:getID(), "No valid targets available",10) + canPurchase = false + end + + elseif itemType == PlayerTracker.cmdShopTypes.jtac then + + self.groupTgtMenus[gr:getID()] = MenuRegistry.showTargetZoneMenu(gr:getID(), "JTAC target", function(params) + + CommandFunctions.spawnJtac(params.zone) + trigger.action.outTextForGroup(params.groupid, "Reaper orbiting "..params.zone.name,5) + + end, 1, 1) + + if self.groupTgtMenus[gr:getID()] then + trigger.action.outTextForGroup(gr:getID(), "Select target from radio menu",10) + else + trigger.action.outTextForGroup(gr:getID(), "No valid targets available",10) + canPurchase = false + end + + elseif itemType== PlayerTracker.cmdShopTypes.prio then + + self.groupTgtMenus[gr:getID()] = MenuRegistry.showTargetZoneMenu(gr:getID(), "Priority zone", function(params) + BattlefieldManager.overridePriority(2, params.zone, 4) + trigger.action.outTextForGroup(params.groupid, "Blue is concentrating efforts on "..params.zone.name.." for the next two hours", 5) + end, nil, 1) + + if self.groupTgtMenus[gr:getID()] then + trigger.action.outTextForGroup(gr:getID(), "Select target from radio menu",10) + else + trigger.action.outTextForGroup(gr:getID(), "No valid targets available",10) + canPurchase = false + end + + elseif itemType== PlayerTracker.cmdShopTypes.bribe1 then + + timer.scheduleFunction(function(params, time) + local count = 0 + for i,v in pairs(ZoneCommand.getAllZones()) do + if v.side == 1 and v.distToFront <= 1 then + if math.random()<0.5 then + v:reveal() + count = count + 1 + end + end + end + if count > 0 then + trigger.action.outTextForGroup(params.groupid, "Intercepted enemy communications have revealed information on "..count.." enemy zones",20) + else + trigger.action.outTextForGroup(params.groupid, "No useful information has been intercepted",20) + end + end, {groupid=gr:getID()}, timer.getTime()+60) + + trigger.action.outTextForGroup(gr:getID(), "Attempting to intercept enemy comms...",60) + + elseif itemType == PlayerTracker.cmdShopTypes.bribe2 then + timer.scheduleFunction(function(params, time) + local count = 0 + for i,v in pairs(ZoneCommand.getAllZones()) do + if v.side == 1 then + if math.random()<0.5 then + v:reveal() + count = count + 1 + end + end + end + + if count > 0 then + trigger.action.outTextForGroup(params.groupid, "Bribed officer has shared intel on "..count.." enemy zones",20) + else + trigger.action.outTextForGroup(params.groupid, "Bribed officer has stopped responding to attempted communications.",20) + end + end, {groupid=gr:getID()}, timer.getTime()+(60*5)) + + trigger.action.outTextForGroup(gr:getID(), "Bribe has been transfered to enemy officer. Waiting for contact...",20) + elseif itemType == PlayerTracker.cmdShopTypes.artillery then + self.groupTgtMenus[gr:getID()] = MenuRegistry.showTargetZoneMenu(gr:getID(), "Artillery target", function(params) + CommandFunctions.shellZone(params.zone, 50) + end, 1, 1) + + if self.groupTgtMenus[gr:getID()] then + trigger.action.outTextForGroup(gr:getID(), "Select target from radio menu",10) + else + trigger.action.outTextForGroup(gr:getID(), "No valid targets available",10) + canPurchase = false + end + + elseif itemType == PlayerTracker.cmdShopTypes.sabotage1 then + self.groupTgtMenus[gr:getID()] = MenuRegistry.showTargetZoneMenu(gr:getID(), "Sabotage target", function(params) + CommandFunctions.sabotageZone(params.zone) + end, 1, 1) + + if self.groupTgtMenus[gr:getID()] then + trigger.action.outTextForGroup(gr:getID(), "Select target from radio menu",10) + else + trigger.action.outTextForGroup(gr:getID(), "No valid targets available",10) + canPurchase = false + end + end + + if canPurchase then + self.stats[player][PlayerTracker.statTypes.cmd] = self.stats[player][PlayerTracker.statTypes.cmd] - cost + end + else + trigger.action.outTextForUnit(un:getID(), "Insufficient CMD to buy selected item", 5) + end + end + end + end + + function PlayerTracker:showFrequencies(groupname) + local gr = Group.getByName(groupname) + if gr then + for i,v in pairs(gr:getUnits()) do + if v.getPlayerName and v:getPlayerName() then + local message = RadioFrequencyTracker.getRadioFrequencyMessage(gr:getCoalition()) + trigger.action.outTextForUnit(v:getID(), message, 20) + end + end + end + end + + function PlayerTracker:validateLandingMenu(groupname) + local gr = Group.getByName(groupname) + if gr then + for i,v in pairs(gr:getUnits()) do + if v.getPlayerName and v:getPlayerName() then + self:validateLanding(v, v:getPlayerName()) + end + end + end + end + + function PlayerTracker:showGroupStats(groupname) + local gr = Group.getByName(groupname) + if gr then + for i,v in pairs(gr:getUnits()) do + if v.getPlayerName and v:getPlayerName() then + local player = v:getPlayerName() + local message = '['..player..']\n' + + local stats = self.stats[player] + if stats then + local xp = stats[PlayerTracker.statTypes.xp] + if xp then + local rank, nextRank = self:getRank(xp) + + message = message ..'\nXP: '..xp + + if rank then + message = message..'\nRank: '..rank.name + end + + if nextRank then + message = message..'\nXP needed for promotion: '..(nextRank.requiredXP-xp) + end + end + + local multiplier = self:getPlayerMultiplier(player) + if multiplier then + message = message..'\nSurvival XP multiplier: '..string.format("%.2f", multiplier)..'x' + + if stats[PlayerTracker.statTypes.survivalBonus] ~= nil then + message = message..' [SECURED]' + end + end + + local cmd = stats[PlayerTracker.statTypes.cmd] + if cmd then + message = message ..'\n\nCMD: '..cmd + end + end + + local tstats = self.tempStats[player] + if tstats then + message = message..'\n' + local tempxp = tstats[PlayerTracker.statTypes.xp] + if tempxp and tempxp > 0 then + message = message .. '\nUnclaimed XP: '..tempxp + end + + local tempcmd = tstats[PlayerTracker.statTypes.cmd] + if tempcmd and tempcmd > 0 then + message = message .. '\nUnclaimed CMD: '..tempcmd + end + end + + trigger.action.outTextForUnit(v:getID(), message, 10) + end + end + end + end + + function PlayerTracker:setPlayerConfig(player, setting, value) + local cfg = self:getPlayerConfig(player) + cfg[setting] = value + end + + function PlayerTracker:getPlayerConfig(player) + if not self.config[player] then + self.config[player] = { + noMissionWarning = false, + gci_warning_radius = nil, + gci_metric = nil + } + end + + return self.config[player] + end + + function PlayerTracker:periodicSave() + timer.scheduleFunction(function(param, time) + param:save() + return time+60 + end, self, timer.getTime()+60) + end + + function PlayerTracker:save() + local tosave = {} + tosave.stats = self.stats + tosave.config = self.config + + tosave.zones = {} + tosave.zones.red = {} + tosave.zones.blue = {} + tosave.zones.neutral = {} + for i,v in pairs(ZoneCommand.getAllZones()) do + if v.side == 1 then + table.insert(tosave.zones.red,v.name) + elseif v.side == 2 then + table.insert(tosave.zones.blue,v.name) + elseif v.side == 0 then + table.insert(tosave.zones.neutral,v.name) + end + end + + tosave.players = {} + for i,v in ipairs(coalition.getPlayers(2)) do + if v and v:isExist() and v.getPlayerName then + table.insert(tosave.players, {name=v:getPlayerName(), unit=v:getDesc().typeName}) + end + end + + Utils.saveTable(PlayerTracker.savefile, tosave) + env.info("PlayerTracker - state saved") + end + + PlayerTracker.ranks = {} + PlayerTracker.ranks[1] = { rank=1, name='E-1 Airman basic', requiredXP = 0, cmdChance = 0, cmdAward=0, cmdTrys=0} + PlayerTracker.ranks[2] = { rank=2, name='E-2 Airman', requiredXP = 2000, cmdChance = 0, cmdAward=0, cmdTrys=0} + PlayerTracker.ranks[3] = { rank=3, name='E-3 Airman first class', requiredXP = 4500, cmdChance = 0, cmdAward=0, cmdTrys=0} + PlayerTracker.ranks[4] = { rank=4, name='E-4 Senior airman', requiredXP = 7700, cmdChance = 0, cmdAward=0, cmdTrys=0} + PlayerTracker.ranks[5] = { rank=5, name='E-5 Staff sergeant', requiredXP = 11800, cmdChance = 0.01, cmdAward=1, cmdTrys=1} + PlayerTracker.ranks[6] = { rank=6, name='E-6 Technical sergeant', requiredXP = 17000, cmdChance = 0.01, cmdAward=5, cmdTrys=10} + PlayerTracker.ranks[7] = { rank=7, name='E-7 Master sergeant', requiredXP = 23500, cmdChance = 0.03, cmdAward=5, cmdTrys=10} + PlayerTracker.ranks[8] = { rank=8, name='E-8 Senior master sergeant', requiredXP = 31500, cmdChance = 0.06, cmdAward=10, cmdTrys=10} + PlayerTracker.ranks[9] = { rank=9, name='E-9 Chief master sergeant', requiredXP = 42000, cmdChance = 0.10, cmdAward=10, cmdTrys=10} + PlayerTracker.ranks[10] = { rank=10, name='O-1 Second lieutenant', requiredXP = 52800, cmdChance = 0.14, cmdAward=20, cmdTrys=15} + PlayerTracker.ranks[11] = { rank=11, name='O-2 First lieutenant', requiredXP = 66500, cmdChance = 0.20, cmdAward=20, cmdTrys=15} + PlayerTracker.ranks[12] = { rank=12, name='O-3 Captain', requiredXP = 82500, cmdChance = 0.27, cmdAward=25, cmdTrys=15, allowCarrierSupport=true} + PlayerTracker.ranks[13] = { rank=13, name='O-4 Major', requiredXP = 101000, cmdChance = 0.34, cmdAward=25, cmdTrys=20, allowCarrierSupport=true} + PlayerTracker.ranks[14] = { rank=14, name='O-5 Lieutenant colonel', requiredXP = 122200, cmdChance = 0.43, cmdAward=25, cmdTrys=20, allowCarrierSupport=true} + PlayerTracker.ranks[15] = { rank=15, name='O-6 Colonel', requiredXP = 146300, cmdChance = 0.52, cmdAward=30, cmdTrys=20, allowCarrierSupport=true} + PlayerTracker.ranks[16] = { rank=16, name='O-7 Brigadier general', requiredXP = 173500, cmdChance = 0.63, cmdAward=35, cmdTrys=25, allowCarrierSupport=true, allowCarrierCommand=true} + PlayerTracker.ranks[17] = { rank=17, name='O-8 Major general', requiredXP = 204000, cmdChance = 0.74, cmdAward=40, cmdTrys=25, allowCarrierSupport=true, allowCarrierCommand=true} + PlayerTracker.ranks[18] = { rank=18, name='O-9 Lieutenant general', requiredXP = 238000, cmdChance = 0.87, cmdAward=45, cmdTrys=25, allowCarrierSupport=true, allowCarrierCommand=true} + PlayerTracker.ranks[19] = { rank=19, name='O-10 General', requiredXP = 275700, cmdChance = 0.95, cmdAward=50, cmdTrys=30, allowCarrierSupport=true, allowCarrierCommand=true} + + function PlayerTracker:getPlayerRank(playername) + if self.stats[playername] then + local xp = self.stats[playername][PlayerTracker.statTypes.xp] + if xp then + return self:getRank(xp) + end + end + end + + function PlayerTracker:getPlayerMultiplier(playername) + if self.playerEarningMultiplier[playername] then + return self.playerEarningMultiplier[playername].multiplier + end + + return 1.0 + end + + function PlayerTracker:getPlayerMinutes(playername) + if self.playerEarningMultiplier[playername] then + return self.playerEarningMultiplier[playername].minutes + end + + return 0 + end + + function PlayerTracker.minutesToMultiplier(minutes) + local multi = 1.0 + if minutes > 10 and minutes <= 60 then + multi = 1.0 + ((minutes-10)*0.05) + elseif minutes > 60 then + multi = 1.0 + (50*0.05) + ((minutes - 60)*0.025) + end + + return math.min(multi, 5.0) + end + + function PlayerTracker:getRank(xp) + local rank = nil + local nextRank = nil + for _, rnk in ipairs(PlayerTracker.ranks) do + if rnk.requiredXP <= xp then + rank = rnk + else + nextRank = rnk + break + end + end + + return rank, nextRank + end +end + +-----------------[[ END OF PlayerTracker.lua ]]----------------- + + + +-----------------[[ ReconManager.lua ]]----------------- + +ReconManager = {} +do + ReconManager.groupMenus = {} + ReconManager.requiredProgress = 5*60 + ReconManager.updateFrequency = 5 + + function ReconManager:new() + local obj = {} + obj.recondata = {} + obj.cancelRequests = {} + + setmetatable(obj, self) + self.__index = self + DependencyManager.register("ReconManager", obj) + obj:init() + return obj + end + + function ReconManager:init() + MenuRegistry:register(7, function(event, context) + if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + local groupname = event.initiator:getGroup():getName() + + if ReconManager.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, ReconManager.groupMenus[groupid]) + ReconManager.groupMenus[groupid] = nil + end + + if not ReconManager.groupMenus[groupid] then + local menu = missionCommands.addSubMenuForGroup(groupid, 'Recon') + missionCommands.addCommandForGroup(groupid, 'Start', menu, Utils.log(context.activateRecon), context, groupname) + missionCommands.addCommandForGroup(groupid, 'Cancel', menu, Utils.log(context.cancelRecon), context, groupname) + missionCommands.addCommandForGroup(groupid, 'Analyze', menu, Utils.log(context.analyzeData), context, groupname) + + ReconManager.groupMenus[groupid] = menu + end + end + elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + + if ReconManager.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, ReconManager.groupMenus[groupid]) + ReconManager.groupMenus[groupid] = nil + end + end + end + end, self) + end + + function ReconManager:activateRecon(groupname) + local gr = Group.getByName(groupname) + if gr then + local un = gr:getUnit(1) + if un and un:isExist() then + timer.scheduleFunction(function(param, time) + local cancelRequest = param.context.cancelRequests[param.groupname] + if cancelRequest and (timer.getAbsTime() - cancelRequest < 5) then + param.context.cancelRequests[param.groupname] = nil + return + end + + local shouldUpdateMsg = (timer.getAbsTime() - param.lastUpdate) > ReconManager.updateFrequency + + local withinParameters = false + + local pgr = Group.getByName(param.groupname) + if not pgr then + return + end + local pun = pgr:getUnit(1) + if not pun or not pun:isExist() then + return + end + + local closestZone = nil + if param.lastZone then + if param.lastZone.side == 0 or param.lastZone.side == pun:getCoalition() then + local msg = param.lastZone.name..' is no longer controlled by the enemy.' + msg = msg..'\n Discarding data.' + trigger.action.outTextForUnit(pun:getID(), msg, 20) + closestZone = ZoneCommand.getClosestZoneToPoint(pun:getPoint(), Utils.getEnemy(pun:getCoalition())) + else + closestZone = param.lastZone + end + else + closestZone = ZoneCommand.getClosestZoneToPoint(pun:getPoint(), Utils.getEnemy(pun:getCoalition())) + end + + if not closestZone then + return + end + + local stats = ReconManager.getAircraftStats(pun:getDesc().typeName) + local currentParameters = { + distance = 0, + deviation = 0, + percent_visible = 0 + } + + currentParameters.distance = mist.utils.get2DDist(pun:getPoint(), closestZone.zone.point) + + local unitPos = pun:getPosition() + local unitheading = math.deg(math.atan2(unitPos.x.z, unitPos.x.x)) + local bearing = Utils.getBearing(pun:getPoint(), closestZone.zone.point) + + currentParameters.deviation = math.abs(Utils.getHeadingDiff(unitheading, bearing)) + + local unitsCount = 0 + local visibleCount = 0 + for _,product in pairs(closestZone.built) do + if product.side ~= pun:getCoalition() then + local gr = Group.getByName(product.name) + if gr then + for _,enemyUnit in ipairs(gr:getUnits()) do + unitsCount = unitsCount+1 + local from = pun:getPoint() + from.y = from.y+1.5 + local to = enemyUnit:getPoint() + to.y = to.y+1.5 + if land.isVisible(from, to) then + visibleCount = visibleCount+1 + end + end + else + local st = StaticObject.getByName(product.name) + if st then + unitsCount = unitsCount+1 + local from = pun:getPoint() + from.y = from.y+1.5 + local to = st:getPoint() + to.y = to.y+1.5 + if land.isVisible(from, to) then + visibleCount = visibleCount+1 + end + end + end + end + end + + if unitsCount > 0 and visibleCount > 0 then + currentParameters.percent_visible = visibleCount/unitsCount + end + + if currentParameters.distance < (stats.minDist * 1000) and currentParameters.percent_visible >= 0.5 then + if stats.maxDeviation then + if currentParameters.deviation <= stats.maxDeviation then + withinParameters = true + end + else + withinParameters = true + end + end + + if withinParameters then + if not param.lastZone then + param.lastZone = closestZone + end + + param.timeout = 300 + + local speed = stats.recon_speed * currentParameters.percent_visible + param.progress = math.min(param.progress + speed, ReconManager.requiredProgress) + + if shouldUpdateMsg then + local msg = "[Recon: "..param.lastZone.name..']' + msg = msg.."\nProgress: "..string.format('%.1f', (param.progress/ReconManager.requiredProgress)*100)..'%\n' + msg = msg.."\nVisibility: "..string.format('%.1f', currentParameters.percent_visible*100)..'%' + trigger.action.outTextForUnit(pun:getID(), msg, ReconManager.updateFrequency) + + param.lastUpdate = timer.getAbsTime() + end + else + param.timeout = param.timeout - 1 + if shouldUpdateMsg then + + local msg = "[Nearest enemy zone: "..closestZone.name..']' + + if param.lastZone then + msg = "[Recon in progress: "..param.lastZone.name..']' + msg = msg.."\nProgress: "..string.format('%.1f', (param.progress/ReconManager.requiredProgress)*100)..'%\n' + end + + if stats.maxDeviation then + msg = msg.."\nDeviation: "..string.format('%.1f', currentParameters.deviation)..' deg (under '..stats.maxDeviation..' deg)' + end + + msg = msg.."\nDistance: "..string.format('%.2f', currentParameters.distance/1000)..'km (under '..stats.minDist..' km)' + msg = msg.."\nVisibility: "..string.format('%.1f', currentParameters.percent_visible*100)..'% (min 50%)' + msg = msg.."\n\nTime left: "..param.timeout..' sec' + trigger.action.outTextForUnit(pun:getID(), msg, ReconManager.updateFrequency) + + param.lastUpdate = timer.getAbsTime() + end + end + + if param.progress >= ReconManager.requiredProgress then + + local msg = "Data recorded for "..param.lastZone.name + msg = msg.."\nAnalyze data at a friendly zone to recover results" + trigger.action.outTextForUnit(pun:getID(), msg, 20) + + param.context.recondata[param.groupname] = param.lastZone + return + end + + if param.timeout > 0 then + return time+1 + end + + local msg = "Recon cancelled." + if param.progress > 0 then + msg = msg.." Data lost." + end + trigger.action.outTextForUnit(pun:getID(), msg, 20) + + end, {context = self, groupname = groupname, timeout = 300, progress = 0, lastZone = nil, lastUpdate = timer.getAbsTime()-5}, timer.getTime()+1) + end + end + end + + function ReconManager:cancelRecon(groupname) + self.cancelRequests[groupname] = timer.getAbsTime() + end + + function ReconManager:analyzeData(groupname) + local gr = Group.getByName(groupname) + if not gr then return end + local un = gr:getUnit(1) + if not un or not un:isExist() then return end + local player = un:getPlayerName() + + local zn = ZoneCommand.getZoneOfUnit(un:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(un:getName()) + end + + if not zn or not Utils.isLanded(un, zn.isCarrier) then + trigger.action.outTextForUnit(un:getID(), "Recon data can only be analyzed while landed in a friendly zone.", 5) + return + end + + local data = self.recondata[groupname] + if data then + if data.side == 0 or data.side == un:getCoalition() then + local msg = param.lastZone.name..' is no longer controlled by the enemy.' + msg = msg..'\n Data discarded.' + trigger.action.outTextForUnit(un:getID(), msg, 20) + else + local wasRevealed = data.revealTime > 60 + data:reveal() + + if data:hasUnitWithAttributeOnSide({'Buildings'}, 1) then + local tgt = data:getRandomUnitWithAttributeOnSide({'Buildings'}, 1) + if tgt then + MissionTargetRegistry.addStrikeTarget(tgt, data) + trigger.action.outTextForUnit(un:getID(), tgt.display..' discovered at '..data.name, 20) + end + end + + local xp = RewardDefinitions.actions.recon * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(player) + if wasRevealed then + xp = xp/10 + end + + DependencyManager.get("PlayerTracker"):addStat(player, math.floor(xp), PlayerTracker.statTypes.xp) + local msg = '+'..math.floor(xp)..' XP' + trigger.action.outTextForUnit(un:getID(), msg, 10) + + DependencyManager.get("MissionTracker"):tallyRecon(player, data.name, zn.name) + end + + self.recondata[groupname] = nil + else + trigger.action.outTextForUnit(un:getID(), "No data recorded.", 5) + end + end + + function ReconManager.getAircraftStats(aircraftType) + local stats = ReconManager.aircraftStats[aircraftType] + if not stats then + stats = { recon_speed = 1, minDist = 5 } + end + + return stats + end + + ReconManager.aircraftStats = { + ['A-10A'] = { recon_speed = 1, minDist = 5, }, + ['A-10C'] = { recon_speed = 2, minDist = 20, }, + ['A-10C_2'] = { recon_speed = 2, minDist = 20, }, + ['A-4E-C'] = { recon_speed = 1, minDist = 5, }, + ['AJS37'] = { recon_speed = 10, minDist = 10, }, + ['AV8BNA'] = { recon_speed = 2, minDist = 20, }, + ['C-101CC'] = { recon_speed = 1, minDist = 5, }, + ['F-14A-135-GR'] = { recon_speed = 10, minDist = 5, }, + ['F-14B'] = { recon_speed = 10, minDist = 5, }, + ['F-15C'] = { recon_speed = 1, minDist = 5, }, + ['F-16C_50'] = { recon_speed = 2, minDist = 20, }, + ['F-5E-3'] = { recon_speed = 1, minDist = 5, }, + ['F-86F Sabre'] = { recon_speed = 1, minDist = 5, }, + ['FA-18C_hornet'] = { recon_speed = 2, minDist = 20, }, + ['Hercules'] = { recon_speed = 1, minDist = 5, }, + ['J-11A'] = { recon_speed = 1, minDist = 5, }, + ['JF-17'] = { recon_speed = 2, minDist = 20, }, + ['L-39ZA'] = { recon_speed = 1, minDist = 5, }, + ['M-2000C'] = { recon_speed = 1, minDist = 5, }, + ['Mirage-F1BE'] = { recon_speed = 1, minDist = 5, }, + ['Mirage-F1CE'] = { recon_speed = 1, minDist = 5, }, + ['Mirage-F1EE'] = { recon_speed = 1, minDist = 5, }, + ['MiG-15bis'] = { recon_speed = 1, minDist = 5, }, + ['MiG-19P'] = { recon_speed = 1, minDist = 5, }, + ['MiG-21Bis'] = { recon_speed = 1, minDist = 5, }, + ['MiG-29A'] = { recon_speed = 1, minDist = 5, }, + ['MiG-29G'] = { recon_speed = 1, minDist = 5, }, + ['MiG-29S'] = { recon_speed = 1, minDist = 5, }, + ['Su-25'] = { recon_speed = 1, minDist = 5, }, + ['Su-25T'] = { recon_speed = 2, minDist = 10, }, + ['Su-27'] = { recon_speed = 1, minDist = 5, }, + ['Su-33'] = { recon_speed = 1, minDist = 5, }, + ['T-45'] = { recon_speed = 1, minDist = 5, }, + ['AH-64D_BLK_II'] = { recon_speed = 5, minDist = 15, maxDeviation = 120 }, + ['Ka-50'] = { recon_speed = 5, minDist = 15, maxDeviation = 35 }, + ['Ka-50_3'] = { recon_speed = 5, minDist = 15, maxDeviation = 35 }, + ['Mi-24P'] = { recon_speed = 5, minDist = 10, maxDeviation = 60 }, + ['Mi-8MT'] = { recon_speed = 1, minDist = 5, maxDeviation = 30 }, + ['SA342L'] = { recon_speed = 5, minDist = 10, maxDeviation = 120 }, + ['SA342M'] = { recon_speed = 10, minDist = 15, maxDeviation = 120 }, + ['SA342Minigun'] = { recon_speed = 2, minDist = 5, maxDeviation = 45 }, + ['UH-1H'] = { recon_speed = 1, minDist = 5, maxDeviation = 30 }, + ['UH-60L'] = { recon_speed = 1, minDist = 5, maxDeviation = 30 } + } +end + +-----------------[[ END OF ReconManager.lua ]]----------------- + + + +-----------------[[ MissionTargetRegistry.lua ]]----------------- + +MissionTargetRegistry = {} +do + MissionTargetRegistry.playerTargetZones = {} + + function MissionTargetRegistry.addZone(zone) + MissionTargetRegistry.playerTargetZones[zone] = true + end + + function MissionTargetRegistry.removeZone(zone) + MissionTargetRegistry.playerTargetZones[zone] = nil + end + + function MissionTargetRegistry.isZoneTargeted(zone) + return MissionTargetRegistry.playerTargetZones[zone] ~= nil + end + + MissionTargetRegistry.baiTargets = {} + + function MissionTargetRegistry.addBaiTarget(target) + MissionTargetRegistry.baiTargets[target.name] = target + env.info('MissionTargetRegistry - bai target added '..target.name) + end + + function MissionTargetRegistry.baiTargetsAvailable(coalition) + local targets = {} + for i,v in pairs(MissionTargetRegistry.baiTargets) do + if v.product.side == coalition then + local tgt = Group.getByName(v.name) + + if not tgt or not tgt:isExist() or tgt:getSize()==0 then + MissionTargetRegistry.removeBaiTarget(v) + elseif not v.state or v.state ~= 'enroute' then + MissionTargetRegistry.removeBaiTarget(v) + else + table.insert(targets, v) + end + end + end + + return #targets > 0 + end + + function MissionTargetRegistry.getRandomBaiTarget(coalition) + local targets = {} + for i,v in pairs(MissionTargetRegistry.baiTargets) do + if v.product.side == coalition then + local tgt = Group.getByName(v.name) + + if not tgt or not tgt:isExist() or tgt:getSize()==0 then + MissionTargetRegistry.removeBaiTarget(v) + elseif not v.state or v.state ~= 'enroute' then + MissionTargetRegistry.removeBaiTarget(v) + else + table.insert(targets, v) + end + end + end + + if #targets == 0 then return end + + local dice = math.random(1,#targets) + + return targets[dice] + end + + function MissionTargetRegistry.removeBaiTarget(target) + MissionTargetRegistry.baiTargets[target.name] = nil + env.info('MissionTargetRegistry - bai target removed '..target.name) + end + + MissionTargetRegistry.strikeTargetExpireTime = 60*60 + MissionTargetRegistry.strikeTargets = {} + + function MissionTargetRegistry.addStrikeTarget(target, zone) + MissionTargetRegistry.strikeTargets[target.name] = {data=target, zone=zone, addedTime = timer.getAbsTime()} + env.info('MissionTargetRegistry - strike target added '..target.name) + end + + function MissionTargetRegistry.strikeTargetsAvailable(coalition, isDeep) + for i,v in pairs(MissionTargetRegistry.strikeTargets) do + if v.data.side == coalition then + local tgt = StaticObject.getByName(v.data.name) + if not tgt then tgt = Group.getByName(v.data.name) end + + if not tgt or not tgt:isExist() then + MissionTargetRegistry.removeStrikeTarget(v) + elseif timer.getAbsTime() - v.addedTime > MissionTargetRegistry.strikeTargetExpireTime then + MissionTargetRegistry.removeStrikeTarget(v) + elseif not isDeep or v.zone.distToFront >= 2 then + return true + end + end + end + + return false + end + + function MissionTargetRegistry.getRandomStrikeTarget(coalition, isDeep) + local targets = {} + for i,v in pairs(MissionTargetRegistry.strikeTargets) do + if v.data.side == coalition then + local tgt = StaticObject.getByName(v.data.name) + if not tgt then tgt = Group.getByName(v.data.name) end + + if not tgt or not tgt:isExist() then + MissionTargetRegistry.removeStrikeTarget(v) + elseif timer.getAbsTime() - v.addedTime > MissionTargetRegistry.strikeTargetExpireTime then + MissionTargetRegistry.removeStrikeTarget(v) + elseif not isDeep or v.zone.distToFront >= 2 then + table.insert(targets, v) + end + end + end + + if #targets == 0 then return end + + local dice = math.random(1,#targets) + + return targets[dice] + end + + function MissionTargetRegistry.getAllStrikeTargets(coalition) + local targets = {} + for i,v in pairs(MissionTargetRegistry.strikeTargets) do + if v.data.side == coalition then + local tgt = StaticObject.getByName(v.data.name) + if not tgt then tgt = Group.getByName(v.data.name) end + + if not tgt or not tgt:isExist() then + MissionTargetRegistry.removeStrikeTarget(v) + elseif timer.getAbsTime() - v.addedTime > MissionTargetRegistry.strikeTargetExpireTime then + MissionTargetRegistry.removeStrikeTarget(v) + else + table.insert(targets, v) + end + end + end + + return targets + end + + function MissionTargetRegistry.removeStrikeTarget(target) + MissionTargetRegistry.strikeTargets[target.data.name] = nil + env.info('MissionTargetRegistry - strike target removed '..target.data.name) + end + + MissionTargetRegistry.extractableSquads = {} + + function MissionTargetRegistry.addSquad(squad) + MissionTargetRegistry.extractableSquads[squad.name] = squad + env.info('MissionTargetRegistry - squad added '..squad.name) + end + + function MissionTargetRegistry.squadsReadyToExtract(onside) + for i,v in pairs(MissionTargetRegistry.extractableSquads) do + local gr = Group.getByName(i) + if gr and gr:isExist() and gr:getSize() > 0 and gr:getCoalition() == onside then + return true + end + end + + return false + end + + function MissionTargetRegistry.getRandomSquad(onside) + local targets = {} + for i,v in pairs(MissionTargetRegistry.extractableSquads) do + local gr = Group.getByName(i) + if gr and gr:isExist() and gr:getSize() > 0 and gr:getCoalition() == onside then + table.insert(targets, v) + end + end + + if #targets == 0 then return end + + local dice = math.random(1,#targets) + + return targets[dice] + end + + function MissionTargetRegistry.removeSquad(squad) + MissionTargetRegistry.extractableSquads[squad.name] = nil + env.info('MissionTargetRegistry - squad removed '..squad.name) + end + + MissionTargetRegistry.extractablePilots = {} + + function MissionTargetRegistry.addPilot(pilot) + MissionTargetRegistry.extractablePilots[pilot.name] = pilot + env.info('MissionTargetRegistry - pilot added '..pilot.name) + end + + function MissionTargetRegistry.pilotsAvailableToExtract() + for i,v in pairs(MissionTargetRegistry.extractablePilots) do + if v.pilot:isExist() and v.pilot:getSize() > 0 and v.remainingTime > 30*60 then + return true + end + end + + return false + end + + function MissionTargetRegistry.getRandomPilot() + local targets = {} + for i,v in pairs(MissionTargetRegistry.extractablePilots) do + if v.pilot:isExist() and v.pilot:getSize() > 0 and v.remainingTime > 30*60 then + table.insert(targets, v) + end + end + + if #targets == 0 then return end + + local dice = math.random(1,#targets) + + return targets[dice] + end + + function MissionTargetRegistry.removePilot(pilot) + MissionTargetRegistry.extractablePilots[pilot.name] = nil + env.info('MissionTargetRegistry - pilot removed '..pilot.name) + end +end + +-----------------[[ END OF MissionTargetRegistry.lua ]]----------------- + + + +-----------------[[ RadioFrequencyTracker.lua ]]----------------- + +RadioFrequencyTracker = {} + +do + RadioFrequencyTracker.radios = {} + + function RadioFrequencyTracker.registerRadio(groupname, name, frequency) + RadioFrequencyTracker.radios[groupname] = {name = name, frequency = frequency} + end + + function RadioFrequencyTracker.getRadioFrequencyMessage(side) + local radios ={} + for i,v in pairs(RadioFrequencyTracker.radios) do + local gr = Group.getByName(i) + if gr and gr:getCoalition()==side then + table.insert(radios, v) + else + RadioFrequencyTracker.radios[i] = nil + end + end + + table.sort(radios, function (a,b) return a.name < b.name end) + + local msg = 'Active frequencies:' + for i,v in ipairs(radios) do + msg = msg..'\n '..v.name..' ['..v.frequency..']' + end + + return msg + end +end + + +-----------------[[ END OF RadioFrequencyTracker.lua ]]----------------- + + + +-----------------[[ PersistenceManager.lua ]]----------------- + +PersistenceManager = {} + +do + + function PersistenceManager:new(path) + local obj = { + path = path, + data = nil + } + + setmetatable(obj, self) + self.__index = self + return obj + end + + function PersistenceManager:restore() + self:restoreZones() + self:restoreAIMissions() + self:restoreBattlefield() + self:restoreCsar() + self:restoreSquads() + self:restoreCarriers() + + timer.scheduleFunction(function(param) + param:restoreStrikeTargets() + end, self, timer.getTime()+5) + end + + function PersistenceManager:restoreZones() + local save = self.data + for i,v in pairs(save.zones) do + local z = ZoneCommand.getZoneByName(i) + if z then + z:setSide(v.side) + z.resource = v.resource + z.revealTime = v.revealTime + z.extraBuildResources = v.extraBuildResources + z.mode = v.mode + z.distToFront = v.distToFront + z.closestEnemyDist = v.closestEnemyDist + for name,data in pairs(v.built) do + local pr = z:getProductByName(name) + z:instantBuild(pr) + + if pr.type == 'defense' and type(data) == "table" then + local unitTypes = {} + for _,typeName in ipairs(data) do + if not unitTypes[typeName] then + unitTypes[typeName] = 0 + end + unitTypes[typeName] = unitTypes[typeName] + 1 + end + + timer.scheduleFunction(function(param, time) + local gr = Group.getByName(param.name) + if gr then + local types = param.data + local toKill = {} + for _,un in ipairs(gr:getUnits()) do + local tp = un:getDesc().typeName + if types[tp] and types[tp] > 0 then + types[tp] = types[tp] - 1 + else + table.insert(toKill, un) + end + end + + for _,un in ipairs(toKill) do + un:destroy() + end + end + end, {data=unitTypes, name=name}, timer.getTime()+2) + end + end + + if v.currentBuild then + local pr = z:getProductByName(v.currentBuild.name) + z:queueBuild(pr, v.currentBuild.side, v.currentBuild.isRepair, v.currentBuild.progress) + end + + if v.currentMissionBuild then + local pr = z:getProductByName(v.currentMissionBuild.name) + z:queueBuild(pr, v.currentMissionBuild.side, false, v.currentMissionBuild.progress) + end + + z:refreshText() + z:refreshSpawnBlocking() + end + end + + end + + function PersistenceManager:restoreAIMissions() + local save = self.data + local instantBuildStates = { + ['uninitialized'] = true, + ['takeoff'] = true, + } + + local reActivateStates = { + ['inair'] = true, + ['enroute'] = true, + ['atdestination'] = true, + ['siege'] = true + } + + for i,v in pairs(save.activeGroups) do + if v.homeName then + if instantBuildStates[v.state] then + local z = ZoneCommand.getZoneByName(v.homeName) + if z then + local pr = z:getProductByName(v.productName) + if z.side == pr.side then + z:instantBuild(pr) + end + end + elseif v.lastMission and reActivateStates[v.state] then + timer.scheduleFunction(function(param, time) + local z = ZoneCommand.getZoneByName(param.homeName) + if z then + z:reActivateMission(param) + end + end, v, timer.getTime()+3) + end + end + end + end + + function PersistenceManager:restoreBattlefield() + local save = self.data + if save.battlefieldManager then + if save.battlefieldManager.priorityZones then + if save.battlefieldManager.priorityZones['1'] then + BattlefieldManager.priorityZones[1] = ZoneCommand.getZoneByName(save.battlefieldManager.priorityZones[1]) + end + + + if save.battlefieldManager.priorityZones['2'] then + BattlefieldManager.priorityZones[2] = ZoneCommand.getZoneByName(save.battlefieldManager.priorityZones[2]) + end + end + + if save.battlefieldManager.overridePriorityZones then + if save.battlefieldManager.overridePriorityZones['1'] then + BattlefieldManager.overridePriorityZones[1] = { + zone = ZoneCommand.getZoneByName(save.battlefieldManager.overridePriorityZones['1'].zone), + ticks = save.battlefieldManager.overridePriorityZones['1'].ticks + } + end + + if save.battlefieldManager.overridePriorityZones['2'] then + BattlefieldManager.overridePriorityZones[2] = { + zone = ZoneCommand.getZoneByName(save.battlefieldManager.overridePriorityZones['2'].zone), + ticks = save.battlefieldManager.overridePriorityZones['2'].ticks + } + end + end + end + end + + function PersistenceManager:restoreCsar() + local save = self.data + if save.csarTracker then + for i,v in pairs(save.csarTracker) do + DependencyManager.get("CSARTracker"):restorePilot(v) + end + end + end + + function PersistenceManager:restoreSquads() + local save = self.data + if save.squadTracker then + for i,v in pairs(save.squadTracker) do + local sdata = nil + if v.isAISpawned then + if v.type == PlayerLogistics.infantryTypes.ambush then + sdata = GroupMonitor.aiSquads.ambush[v.side] + else + sdata = GroupMonitor.aiSquads.manpads[v.side] + end + else + sdata = DependencyManager.get("PlayerLogistics").registeredSquadGroups[v.type] + end + + if sdata then + v.data = sdata + DependencyManager.get("SquadTracker"):restoreInfantry(v) + end + end + end + end + + function PersistenceManager:restoreStrikeTargets() + local save = self.data + if save.strikeTargets then + for i,v in pairs(save.strikeTargets) do + local zone = ZoneCommand.getZoneByName(v.zname) + local product = zone:getProductByName(v.pname) + + MissionTargetRegistry.strikeTargets[i] = { + data = product, + zone = zone, + addedTime = timer.getAbsTime() - v.elapsedTime, + isDeep = isDeep + } + end + end + end + + function PersistenceManager:restoreCarriers() + local save = self.data + if save.carriers then + for i,v in pairs(save.carriers) do + local carrier = CarrierCommand.getCarrierByName(v.name) + if carrier then + carrier.resource = math.min(v.resource, carrier.maxResource) + carrier.weaponStocks = v.weaponStocks or {} + carrier:refreshSpawnBlocking() + + local group = Group.getByName(v.name) + if group then + local vars = { + groupName = group:getName(), + point = v.position.p, + action = 'teleport', + heading = math.atan2(v.position.x.z, v.position.x.x), + initTasks = false, + route = {} + } + + mist.teleportToPoint(vars) + + timer.scheduleFunction(function(param, time) + param:setupRadios() + end, carrier, timer.getTime()+3) + + carrier.navigation.waypoints = v.navigation.waypoints + carrier.navigation.currentWaypoint = nil + carrier.navigation.nextWaypoint = v.navigation.currentWaypoint + carrier.navigation.loop = v.navigation.loop + + if v.supportFlightStates then + for sfsName, sfsData in pairs(v.supportFlightStates) do + local sflight = carrier.supportFlights[sfsName] + if sflight then + if sfsData.state == CarrierCommand.supportStates.inair and sfsData.targetName and sfsData.position then + local zn = ZoneCommand.getZoneByName(sfsData.targetName) + if not zn then + zn = CarrierCommand.getCarrierByName(sfsData.targetName) + end + + if zn then + CarrierCommand.spawnSupport(sflight, zn, sfsData) + end + elseif sfsData.state == CarrierCommand.supportStates.takeoff and sfsData.targetName then + local zn = ZoneCommand.getZoneByName(sfsData.targetName) + if not zn then + zn = CarrierCommand.getCarrierByName(sfsData.targetName) + end + + if zn then + CarrierCommand.spawnSupport(sflight, zn) + end + end + end + end + end + + if v.aliveGroupMembers then + timer.scheduleFunction(function(param, time) + local g = Group.getByName(param.name) + if not g then return end + local grMembers = g:getUnits() + local liveMembers = {} + for _, agm in ipairs(param.aliveGroupMembers) do + liveMembers[agm] = true + end + + for _, gm in ipairs(grMembers) do + if not liveMembers[gm:getName()] then + gm:destroy() + end + end + end, v, timer.getTime()+4) + end + end + end + end + end + end + + function PersistenceManager:canRestore() + if self.data == nil then return false end + + local redExist = false + local blueExist = false + for _,z in pairs(self.data.zones) do + if z.side == 1 and not redExist then redExist = true end + if z.side == 2 and not blueExist then blueExist = true end + + if redExist and blueExist then break end + end + + if not redExist or not blueExist then return false end + + return true + end + + function PersistenceManager:load() + self.data = Utils.loadTable(self.path) + end + + function PersistenceManager:save() + local tosave = {} + + tosave.zones = {} + for i,v in pairs(ZoneCommand.getAllZones()) do + + tosave.zones[i] = { + name = v.name, + side = v.side, + resource = v.resource, + mode = v.mode, + distToFront = v.distToFront, + closestEnemyDist = v.closestEnemyDist, + extraBuildResources = v.extraBuildResources, + revealTime = v.revealTime, + built = {} + } + + for n,b in pairs(v.built) do + if b.type == 'defense' then + local typeList = {} + local gr = Group.getByName(b.name) + if gr then + for _,unit in ipairs(gr:getUnits()) do + table.insert(typeList, unit:getDesc().typeName) + end + + tosave.zones[i].built[n] = typeList + end + else + tosave.zones[i].built[n] = true + end + + end + + if v.currentBuild then + tosave.zones[i].currentBuild = { + name = v.currentBuild.product.name, + progress = v.currentBuild.progress, + side = v.currentBuild.side, + isRepair = v.currentBuild.isRepair + } + end + + if v.currentMissionBuild then + tosave.zones[i].currentMissionBuild = { + name = v.currentMissionBuild.product.name, + progress = v.currentMissionBuild.progress, + side = v.currentMissionBuild.side + } + end + end + + tosave.activeGroups = {} + for i,v in pairs(DependencyManager.get("GroupMonitor").groups) do + tosave.activeGroups[i] = { + productName = v.product.name, + type = v.product.missionType + } + + local gr = Group.getByName(v.product.name) + if gr and gr:getSize()>0 then + local un = gr:getUnit(1) + if un then + tosave.activeGroups[i].position = un:getPoint() + tosave.activeGroups[i].lastMission = v.product.lastMission + tosave.activeGroups[i].heading = math.atan2(un:getPosition().x.z, un:getPosition().x.x) + end + end + + if v.spawnedSquad then + tosave.activeGroups[i].spawnedSquad = true + end + + if v.target then + tosave.activeGroups[i].targetName = v.target.name + end + + if v.home then + tosave.activeGroups[i].homeName = v.home.name + end + + if v.state then + tosave.activeGroups[i].state = v.state + tosave.activeGroups[i].lastStateDuration = timer.getAbsTime() - v.lastStateTime + else + tosave.activeGroups[i].state = 'uninitialized' + tosave.activeGroups[i].lastStateDuration = 0 + end + end + + tosave.battlefieldManager = { + priorityZones = {}, + overridePriorityZones = {} + } + + if BattlefieldManager.priorityZones[1] then + tosave.battlefieldManager.priorityZones['1'] = BattlefieldManager.priorityZones[1].name + end + + if BattlefieldManager.priorityZones[2] then + tosave.battlefieldManager.priorityZones['2'] = BattlefieldManager.priorityZones[2].name + end + + if BattlefieldManager.overridePriorityZones[1] then + tosave.battlefieldManager.overridePriorityZones['1'] = { + zone = BattlefieldManager.overridePriorityZones[1].zone.name, + ticks = BattlefieldManager.overridePriorityZones[1].ticks + } + end + + if BattlefieldManager.overridePriorityZones[2] then + tosave.battlefieldManager.overridePriorityZones['2'] = { + zone = BattlefieldManager.overridePriorityZones[2].zone.name, + ticks = BattlefieldManager.overridePriorityZones[2].ticks + } + end + + + tosave.csarTracker = {} + + for i,v in pairs(DependencyManager.get("CSARTracker").activePilots) do + if v.pilot:isExist() and v.pilot:getSize()>0 and v.remainingTime>60 then + tosave.csarTracker[i] = { + name = v.name, + remainingTime = v.remainingTime, + pos = v.pilot:getUnit(1):getPoint() + } + end + end + + tosave.squadTracker = {} + + for i,v in pairs(DependencyManager.get("SquadTracker").activeInfantrySquads) do + tosave.squadTracker[i] = { + state = v.state, + remainingStateTime = v.remainingStateTime, + position = v.position, + name = v.name, + type = v.data.type, + side = v.data.side, + isAISpawned = v.data.isAISpawned, + discovered = v.discovered + } + end + + tosave.carriers = {} + for cname,cdata in pairs(CarrierCommand.getAllCarriers()) do + local group = Group.getByName(cdata.name) + if group and group:isExist() then + + tosave.carriers[cname] = { + name = cdata.name, + resource = cdata.resource, + position = group:getUnit(1):getPosition(), + navigation = cdata.navigation, + supportFlightStates = {}, + weaponStocks = cdata.weaponStocks, + aliveGroupMembers = {} + } + + for _, gm in ipairs(group:getUnits()) do + table.insert(tosave.carriers[cname].aliveGroupMembers, gm:getName()) + end + + for spname, spdata in pairs(cdata.supportFlights) do + tosave.carriers[cname].supportFlightStates[spname] = { + name = spdata.name, + state = spdata.state, + lastStateDuration = timer.getAbsTime() - spdata.lastStateTime, + returning = spdata.returning + } + + if spdata.target then + tosave.carriers[cname].supportFlightStates[spname].targetName = spdata.target.name + end + + if spdata.state == CarrierCommand.supportStates.inair then + local spgr = Group.getByName(spname) + if spgr and spgr:isExist() and spgr:getSize()>0 then + local spun = spgr:getUnit(1) + if spun and spun:isExist() then + tosave.carriers[cname].supportFlightStates[spname].position = spun:getPoint() + tosave.carriers[cname].supportFlightStates[spname].heading = math.atan2(spun:getPosition().x.z, spun:getPosition().x.x) + end + end + end + end + end + end + + tosave.strikeTargets = {} + for i,v in pairs(MissionTargetRegistry.strikeTargets) do + tosave.strikeTargets[i] = { pname = v.data.name, zname = v.zone.name, elapsedTime = timer.getAbsTime() - v.addedTime, isDeep = v.isDeep } + end + + Utils.saveTable(self.path, tosave) + end +end + +-----------------[[ END OF PersistenceManager.lua ]]----------------- + + + +-----------------[[ TemplateDB.lua ]]----------------- + +TemplateDB = {} + +do + TemplateDB.type = { + group = 'group', + static = 'static', + } + + TemplateDB.templates = {} + function TemplateDB.getData(objtype) + return TemplateDB.templates[objtype] + end + + TemplateDB.templates["pilot-replacement"] = { + units = { "Soldier M4 GRG" }, + skill = "Good", + dataCategory= TemplateDB.type.group + } + + TemplateDB.templates["capture-squad"] = { + units = { + "Soldier M4 GRG", + "Soldier M4 GRG", + "Soldier M249", + "Soldier M4 GRG" + }, + skill = "Good", + dataCategory= TemplateDB.type.group + } + + TemplateDB.templates["sabotage-squad"] = { + units = { + "Soldier M4 GRG", + "Soldier M249", + "Soldier M249", + "Soldier M4 GRG" + }, + skill = "Good", + dataCategory= TemplateDB.type.group + } + + TemplateDB.templates["ambush-squad"] = { + units = { + "Soldier RPG", + "Soldier RPG", + "Soldier M249", + "Soldier M4 GRG", + "Soldier M4 GRG" + }, + skill = "Good", + invisible = true, + dataCategory= TemplateDB.type.group + } + + TemplateDB.templates["manpads-squad"] = { + units = { + "Soldier M4 GRG", + "Soldier M249", + "Soldier stinger", + "Soldier stinger", + "Soldier M4 GRG" + }, + skill = "Good", + dataCategory= TemplateDB.type.group + } + + TemplateDB.templates["ambush-squad-red"] = { + units = { + "Paratrooper RPG-16", + "Paratrooper RPG-16", + "Infantry AK ver2", + "Infantry AK", + "Infantry AK ver3" + }, + skill = "Good", + invisible = true, + dataCategory= TemplateDB.type.group + } + + TemplateDB.templates["manpads-squad-red"] = { + units = { + "Infantry AK ver3", + "Infantry AK ver2", + "SA-18 Igla manpad", + "SA-18 Igla manpad", + "Infantry AK" + }, + skill = "Good", + dataCategory= TemplateDB.type.group + } + + TemplateDB.templates["engineer-squad"] = { + units = { + "Soldier M4 GRG", + "Soldier M4 GRG" + }, + skill = "Good", + dataCategory= TemplateDB.type.group + } + + TemplateDB.templates["spy-squad"] = { + units = { + "Infantry AK" + }, + skill = "Good", + invisible = true, + dataCategory= TemplateDB.type.group + } + + TemplateDB.templates["rapier-squad"] = { + units = { + "rapier_fsa_blindfire_radar", + "rapier_fsa_optical_tracker_unit", + "rapier_fsa_launcher", + "rapier_fsa_launcher", + "Soldier M4 GRG", + "Soldier M4 GRG" + }, + skill = "Excellent", + dataCategory= TemplateDB.type.group + } + + TemplateDB.templates["tent"] = { type="FARP Tent", category="Fortifications", shape="PalatkaB", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["barracks"] = { type="house1arm", category="Fortifications", shape=nil, dataCategory=TemplateDB.type.static } + + TemplateDB.templates["outpost"] = { type="outpost", category="Fortifications", shape=nil, dataCategory=TemplateDB.type.static } + + TemplateDB.templates["ammo-cache"] = { type="FARP Ammo Dump Coating", category="Fortifications", shape="SetkaKP", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["ammo-depot"] = { type=".Ammunition depot", category="Warehouses", shape="SkladC", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["fuel-cache"] = { type="FARP Fuel Depot", category="Fortifications", shape="GSM Rus", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["fuel-tank-small"] = { type="Fuel tank", category="Fortifications", shape="toplivo-bak", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["fuel-tank-big"] = { type="Tank", category="Warehouses", shape="bak", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["chem-tank"] = { type="Chemical tank A", category="Fortifications", shape="him_bak_a", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["factory-1"] = { type="Tech combine", category="Fortifications", shape="kombinat", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["factory-2"] = { type="Workshop A", category="Fortifications", shape="tec_a", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["oil-pump"] = { type="Pump station", category="Fortifications", shape="nasos", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["hangar"] = { type="Hangar A", category="Fortifications", shape="angar_a", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["excavator"] = { type="345 Excavator", category="Fortifications", shape="cat_3451", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["farm-house-1"] = { type="Farm A", category="Fortifications", shape="ferma_a", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["farm-house-2"] = { type="Farm B", category="Fortifications", shape="ferma_b", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["antenna"] = { type="Comms tower M", category="Fortifications", shape="tele_bash_m", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["tv-tower"] = { type="TV tower", category="Fortifications", shape="tele_bash", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["command-center"] = { type=".Command Center", category="Fortifications", shape="ComCenter", dataCategory=TemplateDB.type.static } + + TemplateDB.templates["military-staff"] = { type="Military staff", category="Fortifications", shape="aviashtab", dataCategory=TemplateDB.type.static } +end + +-----------------[[ END OF TemplateDB.lua ]]----------------- + + + +-----------------[[ Spawner.lua ]]----------------- + +Spawner = {} + +do + function Spawner.createPilot(name, pos) + local groupData = Spawner.getData("pilot-replacement", name, pos, nil, 5, { + [land.SurfaceType.LAND] = true, + [land.SurfaceType.ROAD] = true, + [land.SurfaceType.RUNWAY] = true, + }) + + return coalition.addGroup(country.id.CJTF_BLUE, Group.Category.GROUND, groupData) + end + + function Spawner.createObject(name, objType, pos, side, minDist, maxDist, surfaceTypes, zone) + if zone then + zone = CustomZone:getByName(zone) -- expand zone name to CustomZone object + end + + local data = Spawner.getData(objType, name, pos, minDist, maxDist, surfaceTypes, zone) + + if not data then return end + + local cnt = country.id.CJTF_BLUE + if side == 1 then + cnt = country.id.CJTF_RED + end + + if data.dataCategory == TemplateDB.type.static then + return coalition.addStaticObject(cnt, data) + elseif data.dataCategory == TemplateDB.type.group then + return coalition.addGroup(cnt, Group.Category.GROUND, data) + end + end + + function Spawner.getUnit(unitType, name, pos, skill, minDist, maxDist, surfaceTypes, zone) + local nudgedPos = nil + for i=1,500,1 do + nudgedPos = mist.getRandPointInCircle(pos, maxDist, minDist) + + if zone then + if zone:isInside(nudgedPos) and surfaceTypes[land.getSurfaceType(nudgedPos)] then + break + end + else + if surfaceTypes[land.getSurfaceType(nudgedPos)] then + break + end + end + + if i==500 then env.info('Spawner - ERROR: failed to find good location') end + end + + return { + ["type"] = unitType, + ["skill"] = skill, + ["coldAtStart"] = false, + ["x"] = nudgedPos.x, + ["y"] = nudgedPos.y, + ["name"] = name, + ['heading'] = math.random()*math.pi*2, + ["playerCanDrive"] = false + } + end + + function Spawner.getData(objtype, name, pos, minDist, maxDist, surfaceTypes, zone) + if not maxDist then maxDist = 150 end + if not surfaceTypes then surfaceTypes = { [land.SurfaceType.LAND]=true } end + + local data = TemplateDB.getData(objtype) + if not data then + env.info("Spawner - ERROR: cant find group data "..tostring(objtype).." for group name "..name) + return + end + + local spawnData = {} + + if data.dataCategory == TemplateDB.type.static then + if not surfaceTypes[land.getSurfaceType(pos)] then + for i=1,500,1 do + pos = mist.getRandPointInCircle(pos, maxDist) + + if zone then + if zone:isInside(pos) and surfaceTypes[land.getSurfaceType(pos)] then + break + end + else + if surfaceTypes[land.getSurfaceType(pos)] then + break + end + end + + if i==500 then env.info('Spawner - ERROR: failed to find good location') end + end + end + + spawnData = { + ["type"] = data.type, + ["name"] = name, + ["shape_name"] = data.shape, + ["category"] = data.category, + ["x"] = pos.x, + ["y"] = pos.y, + ['heading'] = math.random()*math.pi*2 + } + elseif data.dataCategory== TemplateDB.type.group then + spawnData = { + ["units"] = {}, + ["name"] = name, + ["task"] = "Ground Nothing", + ["route"] = { + ["points"]={ + { + ["x"] = pos.x, + ["y"] = pos.y, + ["action"] = "Off Road", + ["speed"] = 0, + ["type"] = "Turning Point", + ["ETA"] = 0, + ["formation_template"] = "", + ["task"] = Spawner.getDefaultTask(data.invisible) + } + } + } + } + + if data.minDist then + minDist = data.minDist + end + + if data.maxDist then + maxDist = data.maxDist + end + + for i,v in ipairs(data.units) do + table.insert(spawnData.units, Spawner.getUnit(v, name.."-"..i, pos, data.skill, minDist, maxDist, surfaceTypes, zone)) + end + end + + spawnData.dataCategory = data.dataCategory + + return spawnData + end + + function Spawner.getDefaultTask(invisible) + local defTask = { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["enabled"] = true, + ["auto"] = false, + ["id"] = "WrappedAction", + ["number"] = 1, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["name"] = 9, + ["value"] = 2, + }, + }, + }, + }, + [2] = + { + ["enabled"] = true, + ["auto"] = false, + ["id"] = "WrappedAction", + ["number"] = 2, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["name"] = 0, + ["value"] = 0, + } + } + } + } + } + } + } + + if invisible then + table.insert(defTask.params.tasks, { + ["number"] = 3, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "SetInvisible", + ["params"] = + { + ["value"] = true, + } + } + } + }) + end + + return defTask + end +end + +-----------------[[ END OF Spawner.lua ]]----------------- + + + +-----------------[[ CommandFunctions.lua ]]----------------- + +CommandFunctions = {} + +do + CommandFunctions.jtac = nil + + function CommandFunctions.spawnJtac(zone) + if CommandFunctions.jtac then + CommandFunctions.jtac:deployAtZone(zone) + CommandFunctions.jtac:showMenu() + CommandFunctions.jtac:setLifeTime(60) + zone:reveal(60) + end + end + + function CommandFunctions.smokeTargets(zone, count) + local units = {} + for i,v in pairs(zone.built) do + local g = Group.getByName(v.name) + if g and g:isExist() then + for i2,v2 in ipairs(g:getUnits()) do + if v2:isExist() then + table.insert(units, v2) + end + end + else + local s = StaticObject.getByName(v.name) + if s and s:isExist() then + table.insert(units, s) + end + end + end + + local tgts = {} + for i=1,count,1 do + if #units > 0 then + local selected = math.random(1,#units) + table.insert(tgts, units[selected]) + table.remove(units, selected) + end + end + + for i,v in ipairs(tgts) do + local pos = v:getPoint() + trigger.action.smoke(pos, 1) + end + end + + function CommandFunctions.sabotageZone(zone) + trigger.action.outText("Saboteurs have been dispatched to "..zone.name, 10) + local delay = math.random(5*60, 7*60) + local isReveled = zone.revealTime > 0 + timer.scheduleFunction(function(param, time) + if not param.isRevealed then + if math.random() < 0.3 then + trigger.action.outText("Saboteurs have been caught by the enemy before they could complete their mission", 10) + return + end + end + + local zone = param.zone + local units = {} + for i,v in pairs(zone.built) do + if v.type == 'upgrade' then + local s = StaticObject.getByName(v.name) + if s and s:isExist() then + table.insert(units, s) + end + end + end + + if #units > 0 then + local selected = units[math.random(1,#units)] + + timer.scheduleFunction(function(p2, t2) + if p2.count > 0 then + p2.count = p2.count - 1 + local offsetPos = { + x = p2.pos.x + math.random(-25,25), + y = p2.pos.y, + z = p2.pos.z + math.random(-25,25) + } + + offsetPos.y = land.getHeight({x = offsetPos.x, y = offsetPos.z}) + trigger.action.explosion(offsetPos, 30) + return t2 + 0.05 + (math.random()) + else + trigger.action.explosion(p2.pos, 2000) + end + end, {count = 3, pos = selected:getPoint()}, timer.getTime()+0.5) + + trigger.action.outText("Saboteurs have succesfully triggered explosions at "..zone.name, 10) + end + end, { zone = zone , isRevealed = isReveled}, timer.getTime()+delay) + end + + function CommandFunctions.shellZone(zone, count) + local minutes = math.random(3,7) + local seconds = math.random(-30,30) + local delay = (minutes*60)+seconds + + local isRevealed = zone.revealTime > 0 + trigger.action.outText("Artillery preparing to fire on "..zone.name.." ETA: "..minutes.." minutes", 10) + + local positions = {} + for i,v in pairs(zone.built) do + local g = Group.getByName(v.name) + if g and g:isExist() then + for i2,v2 in ipairs(g:getUnits()) do + if v2:isExist() then + table.insert(positions, v2:getPoint()) + end + end + else + local s = StaticObject.getByName(v.name) + if s and s:isExist() then + table.insert(positions, s:getPoint()) + end + end + end + + timer.scheduleFunction(function(param, time) + trigger.action.outText("Artillery firing on "..param.zone.name.." ETA: 30 seconds", 10) + end, {zone = zone}, timer.getTime()+delay-30) + + timer.scheduleFunction(function(param, time) + param.count = param.count - 1 + + local accuracy = 50 + if param.isRevealed then + accuracy = 10 + end + + local selected = param.positions[math.random(1,#param.positions)] + local offsetPos = { + x = selected.x + math.random(-accuracy,accuracy), + y = selected.y, + z = selected.z + math.random(-accuracy,accuracy) + } + + offsetPos.y = land.getHeight({x = offsetPos.x, y = offsetPos.z}) + + trigger.action.explosion(offsetPos, 20) + + if param.count > 0 then + return time+0.05+(math.random()*2) + else + trigger.action.outText("Artillery finished firing on "..param.zone.name, 10) + end + end, { positions = positions, count = count, zone = zone, isRevealed = isRevealed}, timer.getTime()+delay) + end +end + +-----------------[[ END OF CommandFunctions.lua ]]----------------- + + + +-----------------[[ JTAC.lua ]]----------------- + +JTAC = {} +do + JTAC.categories = {} + JTAC.categories['SAM'] = {'SAM SR', 'SAM TR', 'IR Guided SAM','SAM LL','SAM CC'} + JTAC.categories['Infantry'] = {'Infantry'} + JTAC.categories['Armor'] = {'Tanks','IFV','APC'} + JTAC.categories['Support'] = {'Unarmed vehicles','Artillery'} + JTAC.categories['Structures'] = {'StaticObjects'} + + --{name = 'groupname'} + function JTAC:new(obj) + obj = obj or {} + obj.lasers = {tgt=nil, ir=nil} + obj.target = nil + obj.timerReference = nil + obj.remainingLife = nil + obj.tgtzone = nil + obj.priority = nil + obj.jtacMenu = nil + obj.laserCode = 1688 + obj.side = Group.getByName(obj.name):getCoalition() + setmetatable(obj, self) + self.__index = self + obj:initCodeListener() + return obj + end + + function JTAC:initCodeListener() + local ev = {} + ev.context = self + function ev:onEvent(event) + if event.id == 26 then + if event.text:find('^jtac%-code:') then + local s = event.text:gsub('^jtac%-code:', '') + local code = tonumber(s) + self.context:setCode(code) + trigger.action.removeMark(event.idx) + end + end + end + + world.addEventHandler(ev) + end + + function JTAC:setCode(code) + if code>=1111 and code <= 1788 then + self.laserCode = code + trigger.action.outTextForCoalition(self.side, 'JTAC code set to '..code, 10) + else + trigger.action.outTextForCoalition(self.side, 'Invalid laser code. Must be between 1111 and 1788 ', 10) + end + end + + function JTAC:showMenu() + local gr = Group.getByName(self.name) + if not gr then + return + end + + if not self.jtacMenu then + self.jtacMenu = missionCommands.addSubMenuForCoalition(self.side, 'JTAC') + + missionCommands.addCommandForCoalition(self.side, 'Target report', self.jtacMenu, function(dr) + if Group.getByName(dr.name) then + dr:printTarget(true) + else + missionCommands.removeItemForCoalition(dr.side, dr.jtacMenu) + dr.jtacMenu = nil + end + end, self) + + missionCommands.addCommandForCoalition(self.side, 'Next Target', self.jtacMenu, function(dr) + if Group.getByName(dr.name) then + dr:searchTarget() + else + missionCommands.removeItemForCoalition(dr.side, dr.jtacMenu) + dr.jtacMenu = nil + end + end, self) + + missionCommands.addCommandForCoalition(self.side, 'Deploy Smoke', self.jtacMenu, function(dr) + if Group.getByName(dr.name) then + local tgtunit = Unit.getByName(dr.target) + if not tgtunit then + tgtunit = StaticObject.getByName(dr.target) + end + + if tgtunit then + trigger.action.smoke(tgtunit:getPoint(), 3) + trigger.action.outTextForCoalition(dr.side, 'JTAC target marked with ORANGE smoke', 10) + end + else + missionCommands.removeItemForCoalition(dr.side, dr.jtacMenu) + dr.jtacMenu = nil + end + end, self) + + local priomenu = missionCommands.addSubMenuForCoalition(self.side, 'Set Priority', self.jtacMenu) + for i,v in pairs(JTAC.categories) do + missionCommands.addCommandForCoalition(self.side, i, priomenu, function(dr, cat) + if Group.getByName(dr.name) then + dr:setPriority(cat) + dr:searchTarget() + else + missionCommands.removeItemForCoalition(dr.side, dr.jtacMenu) + dr.jtacMenu = nil + end + end, self, i) + end + + local dial = missionCommands.addSubMenuForCoalition(self.side, 'Set Laser Code', self.jtacMenu) + for i2=1,7,1 do + local digit2 = missionCommands.addSubMenuForCoalition(self.side, '1'..i2..'__', dial) + for i3=1,9,1 do + local digit3 = missionCommands.addSubMenuForCoalition(self.side, '1'..i2..i3..'_', digit2) + for i4=1,9,1 do + local digit4 = missionCommands.addSubMenuForCoalition(self.side, '1'..i2..i3..i4, digit3) + local code = tonumber('1'..i2..i3..i4) + missionCommands.addCommandForCoalition(self.side, 'Accept', digit4, Utils.log(self.setCode), self, code) + end + end + end + + missionCommands.addCommandForCoalition(self.side, "Clear", priomenu, function(dr) + if Group.getByName(dr.name) then + dr:clearPriority() + dr:searchTarget() + else + missionCommands.removeItemForCoalition(dr.side, dr.jtacMenu) + dr.jtacMenu = nil + end + end, self) + end + end + + function JTAC:setPriority(prio) + self.priority = JTAC.categories[prio] + self.prioname = prio + end + + function JTAC:clearPriority() + self.priority = nil + end + + function JTAC:setTarget(unit) + + if self.lasers.tgt then + self.lasers.tgt:destroy() + self.lasers.tgt = nil + end + + if self.lasers.ir then + self.lasers.ir:destroy() + self.lasers.ir = nil + end + + local me = Group.getByName(self.name) + if not me then return end + + local pnt = unit:getPoint() + self.lasers.tgt = Spot.createLaser(me:getUnit(1), { x = 0, y = 2.0, z = 0 }, pnt, self.laserCode) + self.lasers.ir = Spot.createInfraRed(me:getUnit(1), { x = 0, y = 2.0, z = 0 }, pnt) + + self.target = unit:getName() + end + + function JTAC:setLifeTime(minutes) + self.remainingLife = minutes + + timer.scheduleFunction(function(param, time) + if param.remainingLife == nil then return end + + local gr = Group.getByName(self.name) + if not gr then + param.remainingLife = nil + return + end + + param.remainingLife = param.remainingLife - 1 + if param.remainingLife < 0 then + param:clearTarget() + return + end + + return time+60 + end, self, timer.getTime()+60) + end + + function JTAC:printTarget(makeitlast) + local toprint = '' + if self.target and self.tgtzone then + local tgtunit = Unit.getByName(self.target) + local isStructure = false + if not tgtunit then + tgtunit = StaticObject.getByName(self.target) + isStructure = true + end + + if tgtunit and tgtunit:isExist() then + local pnt = tgtunit:getPoint() + local tgttype = "Unidentified" + if isStructure then + tgttype = "Structure" + else + tgttype = tgtunit:getTypeName() + end + + if self.priority then + toprint = 'Priority targets: '..self.prioname..'\n' + end + + toprint = toprint..'Lasing '..tgttype..' at '..self.tgtzone.name..'\nCode: '..self.laserCode..'\n' + local lat,lon,alt = coord.LOtoLL(pnt) + local mgrs = coord.LLtoMGRS(coord.LOtoLL(pnt)) + toprint = toprint..'\nDDM: '.. mist.tostringLL(lat,lon,3) + toprint = toprint..'\nDMS: '.. mist.tostringLL(lat,lon,2,true) + toprint = toprint..'\nMGRS: '.. mist.tostringMGRS(mgrs, 5) + toprint = toprint..'\n\nAlt: '..math.floor(alt)..'m'..' | '..math.floor(alt*3.280839895)..'ft' + else + makeitlast = false + toprint = 'No Target' + end + else + makeitlast = false + toprint = 'No target' + end + + local gr = Group.getByName(self.name) + if makeitlast then + trigger.action.outTextForCoalition(gr:getCoalition(), toprint, 60) + else + trigger.action.outTextForCoalition(gr:getCoalition(), toprint, 10) + end + end + + function JTAC:clearTarget() + self.target = nil + + if self.lasers.tgt then + self.lasers.tgt:destroy() + self.lasers.tgt = nil + end + + if self.lasers.ir then + self.lasers.ir:destroy() + self.lasers.ir = nil + end + + if self.timerReference then + timer.removeFunction(self.timerReference) + self.timerReference = nil + end + + local gr = Group.getByName(self.name) + if gr then + gr:destroy() + missionCommands.removeItemForCoalition(self.side, self.jtacMenu) + self.jtacMenu = nil + end + end + + function JTAC:searchTarget() + local gr = Group.getByName(self.name) + if gr and gr:isExist() then + if self.tgtzone and self.tgtzone.side~=0 and self.tgtzone.side~=gr:getCoalition() then + local viabletgts = {} + for i,v in pairs(self.tgtzone.built) do + local tgtgr = Group.getByName(v.name) + if tgtgr and tgtgr:isExist() and tgtgr:getSize()>0 then + for i2,v2 in ipairs(tgtgr:getUnits()) do + if v2:isExist() and v2:getLife()>=1 then + table.insert(viabletgts, v2) + end + end + else + tgtgr = StaticObject.getByName(v.name) + if tgtgr and tgtgr:isExist() then + table.insert(viabletgts, tgtgr) + end + end + end + + if self.priority then + local priorityTargets = {} + for i,v in ipairs(viabletgts) do + for i2,v2 in ipairs(self.priority) do + if v2 == "StaticObjects" and ZoneCommand.staticRegistry[v:getName()] then + table.insert(priorityTargets, v) + break + elseif v:hasAttribute(v2) and v:getLife()>=1 then + table.insert(priorityTargets, v) + break + end + end + end + + if #priorityTargets>0 then + viabletgts = priorityTargets + else + self:clearPriority() + trigger.action.outTextForCoalition(gr:getCoalition(), 'JTAC: No priority targets found', 10) + end + end + + if #viabletgts>0 then + local chosentgt = math.random(1, #viabletgts) + self:setTarget(viabletgts[chosentgt]) + self:printTarget() + else + self:clearTarget() + end + else + self:clearTarget() + end + end + end + + function JTAC:searchIfNoTarget() + if Group.getByName(self.name) then + if not self.target then + self:searchTarget() + else + local un = Unit.getByName(self.target) + if un and un:isExist() then + if un:getLife()>=1 then + self:setTarget(un) + else + self:searchTarget() + end + else + local st = StaticObject.getByName(self.target) + if st and st:isExist() then + self:setTarget(st) + else + self:searchTarget() + end + end + end + else + self:clearTarget() + end + end + + function JTAC:deployAtZone(zoneCom) + self.remainingLife = nil + self.tgtzone = zoneCom + local p = CustomZone:getByName(self.tgtzone.name).point + local vars = {} + vars.gpName = self.name + vars.action = 'respawn' + vars.point = {x=p.x, y=5000, z = p.z} + mist.teleportToPoint(vars) + + timer.scheduleFunction(function(param,time) + param.context:setOrbit(param.target, param.point) + end, {context = self, target = self.tgtzone.zone, point = p}, timer.getTime()+1) + + if not self.timerReference then + self.timerReference = timer.scheduleFunction(function(param, time) + param:searchIfNoTarget() + return time+5 + end, self, timer.getTime()+5) + end + end + + function JTAC:setOrbit(zonename, point) + local gr = Group.getByName(self.name) + if not gr then + return + end + + local cnt = gr:getController() + cnt:setCommand({ + id = 'SetInvisible', + params = { + value = true + } + }) + + cnt:setTask({ + id = 'Orbit', + params = { + pattern = 'Circle', + point = {x = point.x, y=point.z}, + altitude = 5000 + } + }) + + self:searchTarget() + end +end + +-----------------[[ END OF JTAC.lua ]]----------------- + + + +-----------------[[ CarrierMap.lua ]]----------------- + +CarrierMap = {} +do + CarrierMap.currentIndex = 15000 + function CarrierMap:new(zoneList) + + local obj = {} + obj.zones = {} + + for i,v in ipairs(zoneList) do + local zn = CustomZone:getByName(v) + + local id = CarrierMap.currentIndex + CarrierMap.currentIndex = CarrierMap.currentIndex + 1 + + zn:draw(id, {1,1,1,0.2}, {1,1,1,0.2}) + obj.zones[v] = {zone = zn, waypoints = {}} + + for subi=1,1000,1 do + local subname = v..'-'..subi + if CustomZone:getByName(subname) then + table.insert(obj.zones[v].waypoints, subname) + else + break + end + end + + id = CarrierMap.currentIndex + CarrierMap.currentIndex = CarrierMap.currentIndex + 1 + + trigger.action.textToAll(-1, id , zn.point, {0,0,0,0.8}, {1,1,1,0}, 15, true, v) + for i,wps in ipairs(obj.zones[v].waypoints) do + id = CarrierMap.currentIndex + CarrierMap.currentIndex = CarrierMap.currentIndex + 1 + local point = CustomZone:getByName(wps).point + trigger.action.textToAll(-1, id, point, {0,0,0,0.8}, {1,1,1,0}, 10, true, wps) + end + end + + setmetatable(obj, self) + self.__index = self + + return obj + end + + function CarrierMap:getNavMap() + local map = {} + for nm, zn in pairs(self.zones) do + table.insert(map, {name = zn.zone.name, waypoints = zn.waypoints}) + end + + table.sort(map, function(a,b) return a.name < b.name end) + return map + end +end + +-----------------[[ END OF CarrierMap.lua ]]----------------- + + + +-----------------[[ CarrierCommand.lua ]]----------------- + +CarrierCommand = {} +do + CarrierCommand.allCarriers = {} + CarrierCommand.currentIndex = 6000 + CarrierCommand.isCarrier = true + + CarrierCommand.supportTypes = { + strike = 'Strike', + cap = 'CAP', + awacs = 'AWACS', + tanker = 'Tanker', + transport = 'Transport', + mslstrike = 'Cruise Missiles' + } + + CarrierCommand.supportStates = { + takeoff = 'takeoff', + inair = 'inair', + landed = 'landed', + none = 'none' + } + + CarrierCommand.blockedDespawnTime = 10*60 + CarrierCommand.recoveryReduction = 0.8 + CarrierCommand.landedDespawnTime = 10 + + function CarrierCommand:new(name, range, navmap, radioConfig, maxResource) + local unit = Unit.getByName(name) + if not unit then return end + + local obj = {} + obj.name = name + obj.range = range + obj.side = unit:getCoalition() + obj.resource = maxResource or 30000 + obj.maxResource = maxResource or 30000 + obj.spendTreshold = 500 + obj.revealTime = 0 + obj.isHeloSpawn = true + obj.isPlaneSpawn = true + obj.supportFlights = {} + obj.extraSupports = {} + obj.weaponStocks = {} + + obj.navigation = { + currentWaypoint = nil, + waypoints = {}, + loop = true + } + + obj.navmap = navmap + + obj.tacan = radioConfig.tacan + obj.icls = radioConfig.icls + obj.acls = radioConfig.acls + obj.link4 = radioConfig.link4 + obj.radio = radioConfig.radio + + obj.spawns = {} + for i,v in pairs(mist.DBs.groupsByName) do + if v.units[1].skill == 'Client' then + local pos3d = { + x = v.units[1].point.x, + y = 0, + z = v.units[1].point.y + } + + if Utils.isInCircle(pos3d, unit:getPoint(), obj.range)then + table.insert(obj.spawns, {name=i}) + end + end + end + + obj.index = CarrierCommand.currentIndex + CarrierCommand.currentIndex = CarrierCommand.currentIndex + 1 + + local point = unit:getPoint() + + local color = {0.7,0.7,0.7,0.3} + if obj.side == 1 then + color = {1,0,0,0.3} + elseif obj.side == 2 then + color = {0,0,1,0.3} + end + + trigger.action.circleToAll(-1,3000+obj.index,point, obj.range, color, color, 1) + + point.z = point.z + obj.range + trigger.action.textToAll(-1,2000+obj.index, point, {0,0,0,0.8}, {1,1,1,0.5}, 15, true, '') + + setmetatable(obj, self) + self.__index = self + + obj:start() + obj:refreshText() + obj:refreshSpawnBlocking() + CarrierCommand.allCarriers[obj.name] = obj + return obj + end + + function CarrierCommand:setupRadios() + local unit = Unit.getByName(self.name) + TaskExtensions.setupCarrier(unit, self.icls, self.acls, self.tacan, self.link4, self.radio) + end + + function CarrierCommand:start() + self:setupRadios() + + timer.scheduleFunction(function(param, time) + local self = param.context + local unit = Unit.getByName(self.name) + if not unit then + self:clearDrawings() + local gr = Group.getByName(self.name) + if gr and gr:isExist() then + TaskExtensions.stopCarrier(gr) + end + return + end + + self:updateNavigation() + self:updateSupports() + self:refreshText() + return time+10 + end, {context = self}, timer.getTime()+1) + end + + function CarrierCommand:clearDrawings() + if not self.cleared then + trigger.action.removeMark(2000+self.index) + trigger.action.removeMark(3000+self.index) + self.cleared = true + end + end + + function CarrierCommand:updateSupports() + for _, data in pairs(self.supportFlights) do + self:processAir(data) + end + + + for wep, stock in pairs(self.weaponStocks) do + local gr = Unit.getByName(self.name):getGroup() + local realstock = Utils.getAmmo(gr, wep) + self.weaponStocks[wep] = math.min(stock, realstock) + end + end + + local function setState(group, state) + group.state = state + group.lastStateTime = timer.getAbsTime() + end + + local function isAttack(group) + if group.type == CarrierCommand.supportTypes.cap then return true end + if group.type == CarrierCommand.supportTypes.strike then return true end + end + + local function hasWeapons(group) + for _,un in ipairs(group:getUnits()) do + local wps = un:getAmmo() + if wps then + for _,w in ipairs(wps) do + if w.desc.category ~= 0 and w.count > 0 then + return true + end + end + end + end + end + + function CarrierCommand:processAir(group) + local carrier = Unit.getByName(self.name) + if not carrier or not carrier:isExist() then return end + + local gr = Group.getByName(group.name) + if not gr or not gr:isExist() then + if group.state ~= CarrierCommand.supportStates.none then + setState(group, CarrierCommand.supportStates.none) + group.returning = false + env.info('CarrierCommand: processAir ['..group.name..'] does not exist state=none') + end + return + end + + if gr:getSize() == 0 then + gr:destroy() + setState(group, CarrierCommand.supportStates.none) + group.returning = false + env.info('CarrierCommand: processAir ['..group.name..'] has no members state=none') + return + end + + if group.state == CarrierCommand.supportStates.none then + setState(group, CarrierCommand.supportStates.takeoff) + env.info('CarrierCommand: processAir ['..group.name..'] started existing state=takeoff') + elseif group.state == CarrierCommand.supportStates.takeoff then + if timer.getAbsTime() - group.lastStateTime > CarrierCommand.blockedDespawnTime then + if gr and gr:getSize()>0 and gr:getUnit(1):isExist() then + local frUnit = gr:getUnit(1) + local cz = CarrierCommand.getCarrierOfUnit(frUnit:getName()) + if Utils.allGroupIsLanded(gr, cz ~= nil) then + env.info('CarrierCommand: processAir ['..group.name..'] is blocked, despawning') + local frUnit = gr:getUnit(1) + if frUnit then + local firstUnit = frUnit:getName() + local z = ZoneCommand.getZoneOfUnit(firstUnit) + if not z then + z = CarrierCommand.getCarrierOfUnit(firstUnit) + end + if z then + z:addResource(group.cost) + env.info('CarrierCommand: processAir ['..z.name..'] has recovered ['..group.cost..'] from ['..group.name..']') + end + end + + gr:destroy() + setState(group, CarrierCommand.supportStates.none) + group.returning = false + env.info('CarrierCommand: processAir ['..group.name..'] has been removed due to being blocked state=none') + return + end + end + elseif gr and Utils.someOfGroupInAir(gr) then + env.info('CarrierCommand: processAir ['..group.name..'] is in the air state=inair') + setState(group, CarrierCommand.supportStates.inair) + end + elseif group.state == CarrierCommand.supportStates.inair then + if gr and gr:getSize()>0 and gr:getUnit(1) and gr:getUnit(1):isExist() then + local frUnit = gr:getUnit(1) + local cz = CarrierCommand.getCarrierOfUnit(frUnit:getName()) + if Utils.allGroupIsLanded(gr, cz ~= nil) then + env.info('CarrierCommand: processAir ['..group.name..'] has landed state=landed') + setState(group, CarrierCommand.supportStates.landed) + + local unit = gr:getUnit(1) + if unit then + local firstUnit = unit:getName() + local z = ZoneCommand.getZoneOfUnit(firstUnit) + if not z then + z = CarrierCommand.getCarrierOfUnit(firstUnit) + end + + if group.type == CarrierCommand.supportTypes.transport then + if z then + z:capture(gr:getCoalition()) + z:addResource(group.cost) + env.info('CarrierCommand: processAir ['..group.name..'] has supplied ['..z.name..'] with ['..group.cost..']') + end + else + if z and z.side == gr:getCoalition() then + local percentSurvived = gr:getSize()/gr:getInitialSize() + local torecover = math.floor(group.cost * percentSurvived * CarrierCommand.recoveryReduction) + z:addResource(torecover) + env.info('CarrierCommand: processAir ['..z.name..'] has recovered ['..torecover..'] from ['..group.name..']') + end + end + else + env.info('CarrierCommand: processAir ['..group.name..'] size ['..gr:getSize()..'] has no unit 1') + end + else + if isAttack(group) and not group.returning then + if not hasWeapons(gr) then + env.info('CarrierCommand: processAir ['..group.name..'] size ['..gr:getSize()..'] has no weapons outside of shells') + group.returning = true + + local point = carrier:getPoint() + TaskExtensions.landAtAirfield(gr, {x=point.x, y=point.z}) + local cnt = gr:getController() + cnt:setOption(0,4) -- force ai hold fire + cnt:setOption(1, 4) -- force reaction on threat to allow abort + end + elseif group.type == CarrierCommand.supportTypes.transport then + if not group.returning and group.target and group.target.side ~= self.side and group.target.side ~= 0 then + group.returning = true + local point = carrier:getPoint() + TaskExtensions.landAtPointFromAir(gr, {x=point.x, y=point.z}, group.altitude) + env.info('CarrierCommand: processAir ['..group.name..'] returning home due to invalid target') + end + end + end + end + elseif group.state == CarrierCommand.supportStates.landed then + if timer.getAbsTime() - group.lastStateTime > CarrierCommand.landedDespawnTime then + if gr then + gr:destroy() + setState(group, CarrierCommand.supportStates.none) + group.returning = false + env.info('CarrierCommand: processAir ['..group.name..'] despawned after landing state=none') + return true + end + end + end + end + + function CarrierCommand:setWaypoints(wplist) + self.navigation.waypoints = wplist + self.navigation.currentWaypoint = nil + self.navigation.nextWaypoint = 1 + self.navigation.loop = #wplist > 1 + end + + function CarrierCommand:updateNavigation() + local unit = Unit.getByName(self.name) + + if self.navigation.nextWaypoint then + local dist = 0 + if self.navigation.currentWaypoint then + local tgzn = self.navigation.waypoints[self.navigation.currentWaypoint] + local point = CustomZone:getByName(tgzn).point + dist = mist.utils.get2DDist(unit:getPoint(), point) + end + + if dist<2000 then + self.navigation.currentWaypoint = self.navigation.nextWaypoint + + local tgzn = self.navigation.waypoints[self.navigation.currentWaypoint] + local point = CustomZone:getByName(tgzn).point + env.info("CarrierCommand - sending "..self.name.." to "..tgzn.." x"..point.x.." z"..point.z) + TaskExtensions.carrierGoToPos(unit:getGroup(), point) + + if self.navigation.loop then + self.navigation.nextWaypoint = self.navigation.nextWaypoint + 1 + if self.navigation.nextWaypoint > #self.navigation.waypoints then + self.navigation.nextWaypoint = 1 + end + else + self.navigation.nextWaypoint = nil + end + end + else + local dist = 9999999 + if self.navigation.currentWaypoint then + local tgzn = self.navigation.waypoints[self.navigation.currentWaypoint] + local point = CustomZone:getByName(tgzn).point + dist = mist.utils.get2DDist(unit:getPoint(), point) + end + + if dist<2000 then + env.info("CarrierCommand - "..self.name.." stopping after reached waypoint") + TaskExtensions.stopCarrier(unit:getGroup()) + self.navigation.currentWaypoint = nil + end + end + end + + function CarrierCommand:addSupportFlight(name, cost, type, data) + self.supportFlights[name] = { + name = name, + cost = cost, + type = type, + target = nil, + state = CarrierCommand.supportStates.none, + lastStateTime = timer.getAbsTime(), + carrier = self + } + + for i,v in pairs(data) do + self.supportFlights[name][i] = v + end + + local gr = Group.getByName(name) + if gr then gr:destroy() end + end + + function CarrierCommand:addExtraSupport(name, cost, type, data) + self.extraSupports[name] = { + name = name, + cost = cost, + type = type, + target = nil, + carrier = self + } + + for i,v in pairs(data) do + self.extraSupports[name][i] = v + end + end + + function CarrierCommand:setWPStock(wpname, amount) + self.weaponStocks[wpname] = amount + end + + function CarrierCommand:callExtraSupport(data, groupname) + local playerGroup = Group.getByName(groupname) + if not playerGroup then return end + + if self.resource < data.cost then + trigger.action.outTextForGroup(playerGroup:getID(), self.name..' does not have enough resources for '..data.name, 10) + return + end + + local cru = Unit.getByName(self.name) + if not cru or not cru:isExist() then return end + + local crg = cru:getGroup() + + local ammo = self.weaponStocks[data.wpType] or 0 + if ammo < data.salvo then + trigger.action.outTextForGroup(playerGroup:getID(), data.name..' is not available at this time.', 10) + return + end + + local success = MenuRegistry.showTargetZoneMenu(playerGroup:getID(), "Select "..data.name..'('..data.type..") target", function(params) + local cru = Unit.getByName(params.data.carrier.name) + if not cru or not cru:isExist() then return end + local crg = cru:getGroup() + + TaskExtensions.fireAtTargets(crg, params.zone.built, params.data.salvo) + if params.data.carrier.weaponStocks[params.data.wpType] then + params.data.carrier.weaponStocks[params.data.wpType] = params.data.carrier.weaponStocks[params.data.wpType] - params.data.salvo + end + end, 1, nil, data, nil, true) + + if success then + self:removeResource(data.cost) + trigger.action.outTextForGroup(playerGroup:getID(), 'Select target for '..data.name..' ('..data.type..') from radio menu.', 10) + else + trigger.action.outTextForGroup(playerGroup:getID(), 'No valid targets for '..data.name..' ('..data.type..')', 10) + end + end + + function CarrierCommand:callSupport(data, groupname) + local playerGroup = Group.getByName(groupname) + if not playerGroup then return end + + if Group.getByName(data.name) and (timer.getAbsTime() - data.lastStateTime < 60*60) then + trigger.action.outTextForGroup(playerGroup:getID(), data.name..' tasking is not available at this time.', 10) + return + end + + if self.resource < data.cost then + trigger.action.outTextForGroup(playerGroup:getID(), self.name..' does not have enough resources to deploy '..data.name, 10) + return + end + + local targetCoalition = nil + local minDistToFront = nil + local includeCarriers = nil + local onlyRevealed = nil + + if data.type == CarrierCommand.supportTypes.strike then + targetCoalition = 1 + onlyRevealed = true + elseif data.type == CarrierCommand.supportTypes.cap then + minDistToFront = 1 + includeCarriers = true + elseif data.type == CarrierCommand.supportTypes.awacs then + targetCoalition = 2 + includeCarriers = true + elseif data.type == CarrierCommand.supportTypes.tanker then + targetCoalition = 2 + includeCarriers = true + elseif data.type == CarrierCommand.supportTypes.transport then + targetCoalition = {0,2} + end + + local success = MenuRegistry.showTargetZoneMenu(playerGroup:getID(), "Select "..data.name..'('..data.type..") target", function(params) + CarrierCommand.spawnSupport(params.data, params.zone) + trigger.action.outTextForGroup(params.groupid, params.data.name..'('..params.data.type..') heading to '..params.zone.name, 10) + end, targetCoalition, minDistToFront, data, includeCarriers, onlyRevealed) + + if success then + self:removeResource(data.cost) + trigger.action.outTextForGroup(playerGroup:getID(), 'Select target for '..data.name..' ('..data.type..') from radio menu.', 10) + else + trigger.action.outTextForGroup(playerGroup:getID(), 'No valid targets for '..data.name..' ('..data.type..')', 10) + end + end + + local function getDefaultPos(savedData) + local action = 'Turning Point' + local speed = 250 + + local vars = { + groupName = savedData.name, + point = savedData.position, + action = 'respawn', + heading = savedData.heading, + initTasks = false, + route = { + [1] = { + alt = savedData.position.y, + type = 'Turning Point', + action = action, + alt_type = 'BARO', + x = savedData.position.x, + y = savedData.position.z, + speed = speed + } + } + } + + return vars + end + + function CarrierCommand.spawnSupport(data, target, saveData) + data.target = target + + if saveData then + mist.teleportToPoint(getDefaultPos(saveData)) + data.state = saveData.state + data.lastStateTime = timer.getAbsTime() - saveData.lastStateDuration + data.returning = saveData.returning + else + mist.respawnGroup(data.name, true) + end + + if data.type == CarrierCommand.supportTypes.strike then + CarrierCommand.dispatchStrike(data, saveData~=nil) + elseif data.type == CarrierCommand.supportTypes.cap then + CarrierCommand.dispatchCap(data, saveData~=nil) + elseif data.type == CarrierCommand.supportTypes.awacs then + CarrierCommand.dispatchAwacs(data, saveData~=nil) + elseif data.type == CarrierCommand.supportTypes.tanker then + CarrierCommand.dispatchTanker(data, saveData~=nil) + elseif data.type == CarrierCommand.supportTypes.transport then + CarrierCommand.dispatchTransport(data, saveData~=nil) + end + end + + function CarrierCommand.dispatchStrike(data, isReactivated) + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.data.name) + local homePos = nil + local carrier = Unit.getByName(param.data.carrier.name) + if carrier and isReactivated then + homePos = { homePos = carrier:getPoint() } + end + env.info('CarrierCommand - sending '..param.data.name..' to '..param.data.target.name) + + local targets = {} + for i,v in pairs(param.data.target.built) do + if v.type == 'upgrade' and v.side ~= gr:getCoalition() then + local tg = TaskExtensions.getTargetPos(v.name) + table.insert(targets, tg) + end + end + + if #targets == 0 then + gr:destroy() + return + end + + local choice = targets[math.random(1, #targets)] + TaskExtensions.executePinpointStrikeMission(gr, choice, AI.Task.WeaponExpend.ALL, param.data.altitude, homePos, carrier:getID()) + end, {data = data}, timer.getTime()+1) + end + + function CarrierCommand.dispatchCap(data, isReactivated) + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.data.name) + + local homePos = nil + local carrier = Unit.getByName(param.data.carrier.name) + if carrier and isReactivated then + homePos = { homePos = carrier:getPoint() } + end + + local point = nil + if param.data.target.isCarrier then + point = Unit.getByName(param.data.target.name):getPoint() + else + point = trigger.misc.getZone(param.data.target.name).point + end + + TaskExtensions.executePatrolMission(gr, point, param.data.altitude, param.data.range, homePos, carrier:getID()) + end, {data = data}, timer.getTime()+1) + end + + function CarrierCommand.dispatchAwacs(data, isReactivated) + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.data.name) + + local homePos = nil + local carrier = Unit.getByName(param.data.carrier.name) + if carrier and isReactivated then + homePos = { homePos = carrier:getPoint() } + end + + local un = gr:getUnit(1) + if un then + local callsign = un:getCallsign() + RadioFrequencyTracker.registerRadio(param.data.name, '[AWACS] '..callsign, param.data.freq..' AM') + end + + local point = nil + if param.data.target.isCarrier then + point = Unit.getByName(param.data.target.name):getPoint() + else + point = trigger.misc.getZone(param.data.target.name).point + end + + TaskExtensions.executeAwacsMission(gr, point, param.data.altitude, param.data.freq, homePos, carrier:getID()) + end, {data = data}, timer.getTime()+1) + end + + function CarrierCommand.dispatchTanker(data, isReactivated) + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.data.name) + + local homePos = nil + local carrier = Unit.getByName(param.data.carrier.name) + if carrier and isReactivated then + homePos = { homePos = carrier:getPoint() } + end + + local un = gr:getUnit(1) + if un then + local callsign = un:getCallsign() + RadioFrequencyTracker.registerRadio(param.data.name, '[Tanker(Drogue)] '..callsign, param.data.freq..' AM | TCN '..param.data.tacan..'X') + end + + local point = nil + if param.data.target.isCarrier then + point = Unit.getByName(param.data.target.name):getPoint() + else + point = trigger.misc.getZone(param.data.target.name).point + end + + TaskExtensions.executeTankerMission(gr, point, param.data.altitude, param.data.freq, param.data.tacan, homePos, carrier:getID()) + end, {data = data}, timer.getTime()+1) + end + + function CarrierCommand.dispatchTransport(data, isReactivated) + timer.scheduleFunction(function(param) + local gr = Group.getByName(param.data.name) + + local supplyPoint = trigger.misc.getZone(param.data.target.name..'-hsp') + if not supplyPoint then + supplyPoint = trigger.misc.getZone(param.data.target.name) + end + + local point = { x=supplyPoint.point.x, y = supplyPoint.point.z} + TaskExtensions.landAtPoint(gr, point, param.data.altitude, true) + end, {data = data}, timer.getTime()+1) + end + + function CarrierCommand:showInformation(groupname) + local gr = Group.getByName(groupname) + if gr then + local msg = '['..self.name..']' + if self.radio then msg = msg..'\n Radio: '..string.format('%.3f',self.radio/1000000)..' AM' end + if self.tacan then msg = msg..'\n TACAN: '..self.tacan.channel..'X ('..self.tacan.callsign..')' end + if self.link4 then msg = msg..'\n Link4: '..string.format('%.3f',self.link4/1000000) end + if self.icls then msg = msg..'\n ICLS: '..self.icls end + + if Utils.getTableSize(self.supportFlights) > 0 then + local flights = {} + for _, data in pairs(self.supportFlights) do + if (data.state == CarrierCommand.supportStates.none or (timer.getAbsTime()-data.lastStateTime >= 60*60)) and data.cost <= self.resource then + table.insert(flights, data) + end + end + + table.sort(flights, function(a,b) return a.name 0 then + msg = msg..'\n\n Available for tasking:' + for _,data in ipairs(flights) do + msg = msg..'\n '..data.name..' ('..data.type..') ['..data.cost..']' + end + end + end + + if Utils.getTableSize(self.extraSupports) > 0 then + local extras = {} + for _, data in pairs(self.extraSupports) do + if data.cost <= self.resource then + if data.type == CarrierCommand.supportTypes.mslstrike then + local cru = Unit.getByName(self.name) + if cru and cru:isExist() then + local crg = cru:getGroup() + local remaining = self.weaponStocks[data.wpType] or 0 + if remaining > data.salvo then + table.insert(extras, data) + end + end + end + end + end + + table.sort(extras, function(a,b) return a.name 0 then + msg = msg..'\n\n Other:' + for _,data in ipairs(extras) do + if data.type == CarrierCommand.supportTypes.mslstrike then + local cru = Unit.getByName(self.name) + if cru and cru:isExist() then + local crg = cru:getGroup() + local remaining = self.weaponStocks[data.wpType] or 0 + if remaining > data.salvo then + remaining = math.floor(remaining/data.salvo) + msg = msg..'\n '..data.name..' ('..data.type..') ['..data.cost..'] ('..remaining..' left)' + end + end + end + end + end + end + + trigger.action.outTextForGroup(gr:getID(), msg, 20) + end + end + + function CarrierCommand:addResource(amount) + self.resource = self.resource+amount + self.resource = math.floor(math.min(self.resource, self.maxResource)) + self:refreshSpawnBlocking() + self:refreshText() + end + + function CarrierCommand:removeResource(amount) + self.resource = self.resource-amount + self.resource = math.floor(math.max(self.resource, 0)) + self:refreshSpawnBlocking() + self:refreshText() + end + + function CarrierCommand:refreshSpawnBlocking() + for _,v in ipairs(self.spawns) do + trigger.action.setUserFlag(v.name, self.resource < Config.carrierSpawnCost) + end + end + + function CarrierCommand:refreshText() + local build = '' + local mBuild = '' + + local status='' + if self:criticalOnSupplies() then + status = '(!)' + end + + local color = {0.3,0.3,0.3,1} + if self.side == 1 then + color = {0.7,0,0,1} + elseif self.side == 2 then + color = {0,0,0.7,1} + end + + trigger.action.setMarkupColor(2000+self.index, color) + + local label = '['..self.resource..'/'..self.maxResource..']'..status..build..mBuild + + if self.side == 1 then + if self.revealTime > 0 then + trigger.action.setMarkupText(2000+self.index, self.name..label) + else + trigger.action.setMarkupText(2000+self.index, self.name) + end + elseif self.side == 2 then + trigger.action.setMarkupText(2000+self.index, self.name..label) + elseif self.side == 0 then + trigger.action.setMarkupText(2000+self.index, ' '..self.name..' ') + end + + if self.side == 2 and (self.isHeloSpawn or self.isPlaneSpawn) then + trigger.action.setMarkupTypeLine(3000+self.index, 2) + trigger.action.setMarkupColor(3000+self.index, {0,1,0,1}) + end + + local unit = Unit.getByName(self.name) + local point = unit:getPoint() + trigger.action.setMarkupPositionStart(3000+self.index, point) + + point.z = point.z + self.range + trigger.action.setMarkupPositionStart(2000+self.index, point) + end + + function CarrierCommand:capture(side) + end + + function CarrierCommand:criticalOnSupplies() + return self.resource<=self.spendTreshold + end + + function CarrierCommand.getCarrierByName(name) + if not name then return nil end + return CarrierCommand.allCarriers[name] + end + + function CarrierCommand.getAllCarriers() + return CarrierCommand.allCarriers + end + + function CarrierCommand.getCarrierOfUnit(unitname) + local un = Unit.getByName(unitname) + + if not un then + return nil + end + + for i,v in pairs(CarrierCommand.allCarriers) do + local carrier = Unit.getByName(v.name) + if carrier then + if Utils.isInCircle(un:getPoint(), carrier:getPoint(), v.range) then + return v + end + end + end + + return nil + end + + function CarrierCommand.getClosestCarrierToPoint(point) + local minDist = 9999999 + local closest = nil + for i,v in pairs(CarrierCommand.allCarriers) do + local carrier = Unit.getByName(v.name) + if carrier then + local d = mist.utils.get2DDist(carrier:getPoint(), point) + if d < minDist then + minDist = d + closest = v + end + end + end + + return closest, minDist + end + + function CarrierCommand.getCarrierOfPoint(point) + for i,v in pairs(CarrierCommand.allCarriers) do + local carrier = Unit.getByName(v.name) + if carrier then + if Utils.isInCircle(point, carrier:getPoint(), v.range) then + return v + end + end + end + + return nil + end + + CarrierCommand.groupMenus = {} + MenuRegistry:register(6, function(event, context) + if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + local groupname = event.initiator:getGroup():getName() + + if CarrierCommand.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, CarrierCommand.groupMenus[groupid]) + CarrierCommand.groupMenus[groupid] = nil + end + + if not CarrierCommand.groupMenus[groupid] then + + local menu = missionCommands.addSubMenuForGroup(groupid, 'Naval Command') + + local sorted = {} + for cname, carrier in pairs(CarrierCommand.getAllCarriers()) do + local cr = Unit.getByName(carrier.name) + if cr then + table.insert(sorted, carrier) + end + end + + table.sort(sorted, function(a,b) return a.name < b.name end) + + for _,carrier in ipairs(sorted) do + local crunit = Unit.getByName(carrier.name) + if crunit and crunit:isExist() then + local subm = missionCommands.addSubMenuForGroup(groupid, carrier.name, menu) + missionCommands.addCommandForGroup(groupid, 'Information', subm, Utils.log(carrier.showInformation), carrier, groupname) + + local rank = DependencyManager.get("PlayerTracker"):getPlayerRank(player) + + if rank and rank.allowCarrierSupport and Utils.getTableSize(carrier.supportFlights) > 0 then + local supm = missionCommands.addSubMenuForGroup(groupid, "Support", subm) + local flights = {} + for _, data in pairs(carrier.supportFlights) do + table.insert(flights, data) + end + + table.sort(flights, function(a,b) return a.name 1 then + missionCommands.addCommandForGroup(groupid, 'Patrol Area', wpm, Utils.log(carrier.setWaypoints), carrier, wp.waypoints, groupname) + end + + missionCommands.addCommandForGroup(groupid, 'Go to '..wp.name, wpm, Utils.log(carrier.setWaypoints), carrier, {wp.name}, groupname) + for _,subwp in ipairs(wp.waypoints) do + missionCommands.addCommandForGroup(groupid, 'Go to '..subwp, wpm, Utils.log(carrier.setWaypoints), carrier, {subwp}, groupname) + end + end + end + end + end + + CarrierCommand.groupMenus[groupid] = menu + end + end + elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + + if CarrierCommand.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, CarrierCommand.groupMenus[groupid]) + CarrierCommand.groupMenus[groupid] = nil + end + end + end + end, nil) +end + +-----------------[[ END OF CarrierCommand.lua ]]----------------- + + + +-----------------[[ Objectives/Objective.lua ]]----------------- + +Objective = {} + +do + Objective.types = { + fly_to_zone_seq = 'fly_to_zone_seq', -- any of playerlist inside [zone] in sequence + recon_zone = 'recon_zone', -- within X km, facing Y angle +-, % of enemy units in LOS progress faster + destroy_attr = 'destroy_attr', -- any of playerlist kill event on target with any of [attribute] + destroy_attr_at_zone = 'destroy_attr_at_zone', -- any of playerlist kill event on target at [zone] with any of [attribute] + clear_attr_at_zone = 'clear_attr_at_zone', -- [zone] does not have any units with [attribute] + destroy_structure = 'destroy_structure', -- [structure] is killed by any player (getDesc().displayName or getDesc().typeName:gsub('%.','') must match) + destroy_group = 'destroy_group', -- [group] is missing from mission AND any player killed unit from group at least once + supply = 'supply', -- any of playerlist unload [amount] supply at [zone] + extract_pilot = 'extract_pilot', -- players extracted specific ejected pilots + extract_squad = 'extract_squad', -- players extracted specific squad + unloaded_pilot_or_squad = 'unloaded_pilot_or_squad', -- unloaded pilot or squad + deploy_squad = 'deploy_squad', --deploy squad at zone + escort = 'escort', -- escort convoy + protect = 'protect', -- protect other mission + air_kill_bonus = 'air_kill_bonus', -- award bonus for air kills + bomb_in_zone = 'bomb_in_zone', -- bombs tallied inside zone + player_close_to_zone = 'player_close_to_zone' -- player is close to point + } + + function Objective:new(type) + + local obj = { + type = type, + mission = nil, + param = {}, + isComplete = false, + isFailed = false + } + + setmetatable(obj, self) + self.__index = self + + return obj + end + + function Objective:initialize(mission, param) + self.mission = mission + self:validateParameters(param) + self.param = param + end + + function Objective:getType() + return self.type + end + + function Objective:validateParameters(param) + for i,v in pairs(self.requiredParams) do + if v and param[i] == nil then + env.error("Objective - missing parameter: "..i..' in '..self:getType(), true) + end + end + end + + -- virtual + Objective.requiredParams = {} + + function Objective:getText() + env.error("Objective - getText not implemented") + return "NOT IMPLEMENTED" + end + + function Objective:update() + env.error("Objective - update not implemented") + end + + function Objective:checkFail() + env.error("Objective - checkFail not implemented") + end + --end virtual +end + +-----------------[[ END OF Objectives/Objective.lua ]]----------------- + + + +-----------------[[ Objectives/ObjAirKillBonus.lua ]]----------------- + +ObjAirKillBonus = Objective:new(Objective.types.air_kill_bonus) +do + ObjAirKillBonus.requiredParams = { + ['attr'] = true, + ['bonus'] = true, + ['count'] = true, + ['linkedObjectives'] = true + } + + function ObjAirKillBonus:getText() + local msg = 'Destroy: ' + for _,v in ipairs(self.param.attr) do + msg = msg..v..', ' + end + msg = msg:sub(1,#msg-2) + msg = msg..'\n Kills increase mission reward (Ends when other objectives are completed)' + msg = msg..'\n Kills: '..self.param.count + return msg + end + + function ObjAirKillBonus:update() + if not self.isComplete and not self.isFailed then + local allcomplete = true + for _,obj in pairs(self.param.linkedObjectives) do + if obj.isFailed then self.isFailed = true end + if not obj.isComplete then allcomplete = false end + end + + self.isComplete = allcomplete + end + end + + function ObjAirKillBonus:checkFail() + if not self.isComplete and not self.isFailed then + local allcomplete = true + for _,obj in pairs(self.param.linkedObjectives) do + if obj.isFailed then self.isFailed = true end + if not obj.isComplete then allcomplete = false end + end + + self.isComplete = allcomplete + end + end +end + +-----------------[[ END OF Objectives/ObjAirKillBonus.lua ]]----------------- + + + +-----------------[[ Objectives/ObjBombInsideZone.lua ]]----------------- + +ObjBombInsideZone = Objective:new(Objective.types.bomb_in_zone) +do + ObjBombInsideZone.requiredParams = { + ['targetZone'] = true, + ['max'] = true, + ['required'] = true, + ['dropped'] = true, + ['isFinishStarted'] = true, + ['bonus'] = true + } + + function ObjBombInsideZone:getText() + local msg = 'Bomb runways at '..self.param.targetZone.name..'\n' + + local ratio = self.param.dropped/self.param.required + local percent = string.format('%.1f',ratio*100) + + msg = msg..'\n Runway bombed: '..percent..'%\n' + + msg = msg..'\n Cluster bombs do not deal enough damage to complete this mission' + + return msg + end + + function ObjBombInsideZone:update() + if not self.isComplete and not self.isFailed then + if self.param.targetZone.side ~= 1 then + self.isFailed = true + self.mission.failureReason = self.param.targetZone.name..' is no longer controlled by the enemy.' + end + + if not self.param.isFinishStarted then + if self.param.dropped >= self.param.required then + self.param.isFinishStarted = true + timer.scheduleFunction(function(o) + o.isComplete = true + end, self, timer.getTime()+5) + end + end + end + end + + function ObjBombInsideZone:checkFail() + if not self.isComplete and not self.isFailed then + if self.param.targetZone.side ~= 1 then + self.isFailed = true + end + end + end +end + +-----------------[[ END OF Objectives/ObjBombInsideZone.lua ]]----------------- + + + +-----------------[[ Objectives/ObjClearZoneOfUnitsWithAttribute.lua ]]----------------- + +ObjClearZoneOfUnitsWithAttribute = Objective:new(Objective.types.clear_attr_at_zone) +do + ObjClearZoneOfUnitsWithAttribute.requiredParams = { + ['attr'] = true, + ['tgtzone'] = true + } + + function ObjClearZoneOfUnitsWithAttribute:getText() + local msg = 'Clear '..self.param.tgtzone.name..' of: ' + for _,v in ipairs(self.param.attr) do + msg = msg..v..', ' + end + msg = msg:sub(1,#msg-2) + msg = msg..'\n Progress: '..self.param.tgtzone:getUnitCountWithAttributeOnSide(self.param.attr, 1)..' left' + return msg + end + + function ObjClearZoneOfUnitsWithAttribute:update() + if not self.isComplete and not self.isFailed then + local zn = self.param.tgtzone + if zn.side ~= 1 or not zn:hasUnitWithAttributeOnSide(self.param.attr, 1) then + self.isComplete = true + return true + end + end + end + + function ObjClearZoneOfUnitsWithAttribute:checkFail() + -- can not fail + end +end + +-----------------[[ END OF Objectives/ObjClearZoneOfUnitsWithAttribute.lua ]]----------------- + + + +-----------------[[ Objectives/ObjDestroyGroup.lua ]]----------------- + +ObjDestroyGroup = Objective:new(Objective.types.destroy_group) +do + ObjDestroyGroup.requiredParams = { + ['target'] = true, + ['targetUnitNames'] = true, + ['lastUpdate'] = true + } + + function ObjDestroyGroup:getText() + local msg = 'Destroy '..self.param.target.product.display..' before it reaches its destination.\n' + + local gr = Group.getByName(self.param.target.name) + if gr and gr:getSize()>0 then + local killcount = 0 + for i,v in pairs(self.param.targetUnitNames) do + if v == true then + killcount = killcount + 1 + end + end + + msg = msg..'\n '..gr:getSize()..' units remaining. (killed '..killcount..')\n' + for name, unit in pairs(self.mission.players) do + if unit and unit:isExist() then + local tgtUnit = gr:getUnit(1) + local dist = mist.utils.get2DDist(unit:getPoint(), tgtUnit:getPoint()) + + local m = '\n '..name..': Distance: ' + m = m..string.format('%.2f',dist/1000)..'km' + m = m..' Bearing: '..math.floor(Utils.getBearing(unit:getPoint(), tgtUnit:getPoint())) + msg = msg..m + end + end + end + + return msg + end + + function ObjDestroyGroup:update() + if not self.isComplete and not self.isFailed then + local target = self.param.target + local exists = false + local gr = Group.getByName(target.name) + + if gr and gr:getSize() > 0 then + local updateFrequency = 5 -- seconds + local shouldUpdateMsg = (timer.getAbsTime() - self.param.lastUpdate) > updateFrequency + + if shouldUpdateMsg then + for _, unit in pairs(self.mission.players) do + if unit and unit:isExist() then + local tgtUnit = gr:getUnit(1) + local dist = mist.utils.get2DDist(unit:getPoint(), tgtUnit:getPoint()) + local dstkm = string.format('%.2f',dist/1000) + local dstnm = string.format('%.2f',dist/1852) + + local m = 'Distance: ' + m = m..dstkm..'km | '..dstnm..'nm' + + m = m..'\nBearing: '..math.floor(Utils.getBearing(unit:getPoint(), tgtUnit:getPoint())) + trigger.action.outTextForUnit(unit:getID(), m, updateFrequency) + end + end + + self.param.lastUpdate = timer.getAbsTime() + end + elseif target.state == 'enroute' then + for i,v in pairs(self.param.targetUnitNames) do + if v == true then + self.isComplete = true + return true + end + end + + self.isFailed = true + self.mission.failureReason = 'Convoy was killed by someone else.' + return true + else + self.isFailed = true + self.mission.failureReason = 'Convoy has reached its destination.' + return true + end + end + end + + function ObjDestroyGroup:checkFail() + if not self.isComplete and not self.isFailed then + local target = self.param.target + local gr = Group.getByName(target.name) + + if target.state ~= 'enroute' or not gr or gr:getSize() == 0 then + self.isFailed = true + end + end + end +end + +-----------------[[ END OF Objectives/ObjDestroyGroup.lua ]]----------------- + + + +-----------------[[ Objectives/ObjDestroyStructure.lua ]]----------------- + +ObjDestroyStructure = Objective:new(Objective.types.destroy_structure) +do + ObjDestroyStructure.requiredParams = { + ['target']=true, + ['tgtzone']=true, + ['killed']=true + } + + function ObjDestroyStructure:getText() + local msg = 'Destroy '..self.param.target.display..' at '..self.param.tgtzone.name..'\n' + + local point = nil + local st = StaticObject.getByName(self.param.target.name) + if st then + point = st:getPoint() + else + st = Group.getByName(self.param.target.name) + if st and st:getSize()>0 then + point = st:getUnit(1):getPoint() + end + end + + if point then + local lat,lon,alt = coord.LOtoLL(point) + local mgrs = coord.LLtoMGRS(coord.LOtoLL(point)) + msg = msg..'\n DDM: '.. mist.tostringLL(lat,lon,3) + msg = msg..'\n DMS: '.. mist.tostringLL(lat,lon,2,true) + msg = msg..'\n MGRS: '.. mist.tostringMGRS(mgrs, 5) + msg = msg..'\n Altitude: '..math.floor(alt)..'m'..' | '..math.floor(alt*3.280839895)..'ft' + end + + return msg + end + + function ObjDestroyStructure:update() + if not self.isComplete and not self.isFailed then + if self.param.killed then + self.isComplete = true + return true + end + + local target = self.param.target + local exists = false + local st = StaticObject.getByName(target.name) + if st then + exists = true + else + st = Group.getByName(target.name) + if st and st:getSize()>0 then + exists = true + end + end + + if not exists then + if not self.firstFailure then + self.firstFailure = timer.getAbsTime() + end + end + + if self.firstFailure and (timer.getAbsTime() - self.firstFailure > 1*60) then + self.isFailed = true + self.mission.failureReason = 'Structure was destoyed by someone else.' + return true + end + end + end + + function ObjDestroyStructure:checkFail() + if not self.isComplete and not self.isFailed then + local target = self.param.target + local exists = false + local st = StaticObject.getByName(target.name) + if st then + exists = true + else + st = Group.getByName(target.name) + if st and st:getSize()>0 then + exists = true + end + end + + if not exists then + self.isFailed = true + end + end + end +end + +-----------------[[ END OF Objectives/ObjDestroyStructure.lua ]]----------------- + + + +-----------------[[ Objectives/ObjDestroyUnitsWithAttribute.lua ]]----------------- + +ObjDestroyUnitsWithAttribute = Objective:new(Objective.types.destroy_attr) +do + ObjDestroyUnitsWithAttribute.requiredParams = { + ['attr'] = true, + ['amount'] = true, + ['killed'] = true + } + + function ObjDestroyUnitsWithAttribute:getText() + local msg = 'Destroy: ' + for _,v in ipairs(self.param.attr) do + msg = msg..v..', ' + end + msg = msg:sub(1,#msg-2) + msg = msg..'\n Progress: '..self.param.killed..'/'..self.param.amount + return msg + end + + function ObjDestroyUnitsWithAttribute:update() + if not self.isComplete and not self.isFailed then + if self.param.killed >= self.param.amount then + self.isComplete = true + return true + end + end + end + + function ObjDestroyUnitsWithAttribute:checkFail() + -- can not fail + end +end + +-----------------[[ END OF Objectives/ObjDestroyUnitsWithAttribute.lua ]]----------------- + + + +-----------------[[ Objectives/ObjDestroyUnitsWithAttributeAtZone.lua ]]----------------- + +ObjDestroyUnitsWithAttributeAtZone = Objective:new(Objective.types.destroy_attr_at_zone) +do + ObjDestroyUnitsWithAttributeAtZone.requiredParams = { + ['attr']=true, + ['amount'] = true, + ['killed'] = true, + ['tgtzone'] = true + } + + function ObjDestroyUnitsWithAttributeAtZone:getText() + local msg = 'Destroy at '..self.param.tgtzone.name..': ' + for _,v in ipairs(self.param.attr) do + msg = msg..v..', ' + end + msg = msg:sub(1,#msg-2) + msg = msg..'\n Progress: '..self.param.killed..'/'..self.param.amount + return msg + end + + function ObjDestroyUnitsWithAttributeAtZone:update() + if not self.isComplete and not self.isFailed then + if self.param.killed >= self.param.amount then + self.isComplete = true + return true + end + + local zn = self.param.tgtzone + if zn.side ~= 1 or not zn:hasUnitWithAttributeOnSide(self.param.attr, 1) then + if self.firstFailure == nil then + self.firstFailure = timer.getAbsTime() + else + if timer.getAbsTime() - self.firstFailure > 5*60 then + self.isFailed = true + self.mission.failureReason = zn.name..' no longer has targets matching the description.' + return true + end + end + else + if self.firstFailure ~= nil then + self.firstFailure = nil + end + end + end + end + + function ObjDestroyUnitsWithAttributeAtZone:checkFail() + if not self.isComplete and not self.isFailed then + local zn = self.param.tgtzone + if zn.side ~= 1 or not zn:hasUnitWithAttributeOnSide(self.param.attr, 1) then + self.isFailed = true + end + end + end +end + +-----------------[[ END OF Objectives/ObjDestroyUnitsWithAttributeAtZone.lua ]]----------------- + + + +-----------------[[ Objectives/ObjEscortGroup.lua ]]----------------- + +ObjEscortGroup = Objective:new(Objective.types.escort) +do + ObjEscortGroup.requiredParams = { + ['maxAmount']=true, + ['amount'] = true, + ['proxDist']= true, + ['target'] = true, + ['lastUpdate']= true + } + + function ObjEscortGroup:getText() + local msg = 'Stay in close proximity of the convoy' + + local gr = Group.getByName(self.param.target.name) + if gr and gr:getSize()>0 then + local grunit = gr:getUnit(1) + local lat,lon,alt = coord.LOtoLL(grunit:getPoint()) + local mgrs = coord.LLtoMGRS(coord.LOtoLL(grunit:getPoint())) + msg = msg..'\n DDM: '.. mist.tostringLL(lat,lon,3) + msg = msg..'\n DMS: '.. mist.tostringLL(lat,lon,2,true) + msg = msg..'\n MGRS: '.. mist.tostringMGRS(mgrs, 5) + end + + local prg = math.floor(((self.param.maxAmount - self.param.amount)/self.param.maxAmount)*100) + msg = msg.. '\n Progress: '..prg..'%' + return msg + end + + function ObjEscortGroup:update() + if not self.isComplete and not self.isFailed then + local gr = Group.getByName(self.param.target.name) + if not gr or gr:getSize()==0 then + self.isFailed = true + self.mission.failureReason = 'Group has been destroyed.' + return true + end + local grunit = gr:getUnit(1) + + if self.param.target.state == 'atdestination' or self.param.target.state == 'siege' then + for name, unit in pairs(self.mission.players) do + if unit and unit:isExist() then + local dist = mist.utils.get3DDist(unit:getPoint(), grunit:getPoint()) + if dist < self.param.proxDist then + self.isComplete = true + break + end + end + end + + if not self.isComplete then + self.isFailed = true + self.mission.failureReason = 'Group has reached its destination without an escort.' + end + end + + if not self.isComplete and not self.isFailed then + local plycount = Utils.getTableSize(self.mission.players) + if plycount == 0 then plycount = 1 end + local updateFrequency = 5 -- seconds + local shouldUpdateMsg = (timer.getAbsTime() - self.param.lastUpdate) > updateFrequency + for name, unit in pairs(self.mission.players) do + if unit and unit:isExist() then + local dist = mist.utils.get3DDist(unit:getPoint(), grunit:getPoint()) + if dist < self.param.proxDist then + self.param.amount = self.param.amount - (1/plycount) + + if shouldUpdateMsg then + local prg = string.format('%.1f',((self.param.maxAmount - self.param.amount)/self.param.maxAmount)*100) + trigger.action.outTextForUnit(unit:getID(), 'Progress: '..prg..'%', updateFrequency) + end + else + if shouldUpdateMsg then + local m = 'Distance: ' + if dist>1000 then + local dstkm = string.format('%.2f',dist/1000) + local dstnm = string.format('%.2f',dist/1852) + + m = m..dstkm..'km | '..dstnm..'nm' + else + local dstft = math.floor(dist/0.3048) + m = m..math.floor(dist)..'m | '..dstft..'ft' + end + + m = m..'\nBearing: '..math.floor(Utils.getBearing(unit:getPoint(), grunit:getPoint())) + trigger.action.outTextForUnit(unit:getID(), m, updateFrequency) + end + end + end + end + + if shouldUpdateMsg then + self.param.lastUpdate = timer.getAbsTime() + end + end + + if self.param.amount <= 0 then + self.isComplete = true + return true + end + end + end + + function ObjEscortGroup:checkFail() + if not self.isComplete and not self.isFailed then + local tg = self.param.target + local gr = Group.getByName(tg.name) + if not gr or gr:getSize() == 0 then + self.isFailed = true + end + + if self.mission.state == Mission.states.new then + if tg.state == 'enroute' and (timer.getAbsTime() - tg.lastStateTime) >= 7*60 then + self.isFailed = true + end + end + end + end +end + +-----------------[[ END OF Objectives/ObjEscortGroup.lua ]]----------------- + + + +-----------------[[ Objectives/ObjFlyToZoneSequence.lua ]]----------------- + +ObjFlyToZoneSequence = Objective:new(Objective.types.fly_to_zone_seq) +do + ObjFlyToZoneSequence.requiredParams = { + ['waypoints'] = true, + ['failZones'] = true + } + + function ObjFlyToZoneSequence:getText() + local msg = 'Fly route: ' + + for i,v in ipairs(self.param.waypoints) do + if v.complete then + msg = msg..'\n [✓] '..i..'. '..v.zone.name + else + msg = msg..'\n --> '..i..'. '..v.zone.name + end + end + return msg + end + + function ObjFlyToZoneSequence:update() + if not self.isComplete and not self.isFailed then + if self.param.failZones[1] then + for _,zn in ipairs(self.param.failZones[1]) do + if zn.side ~= 1 then + self.isFailed = true + self.mission.failureReason = zn.name..' is no longer controlled by the enemy.' + break + end + end + end + + if self.param.failZones[2] then + for _,zn in ipairs(self.param.failZones[2]) do + if zn.side ~= 2 then + self.isFailed = true + self.mission.failureReason = zn.name..' was lost.' + break + end + end + end + + if not self.isFailed then + local firstWP = nil + local nextWP = nil + for i,leg in ipairs(self.param.waypoints) do + if not leg.complete then + firstWP = leg + nextWP = self.param.waypoints[i+1] + break + end + end + + if firstWP then + local point = firstWP.zone.zone.point + local range = 3000 --meters + local allInside = true + for p,u in pairs(self.mission.players) do + if u and u:isExist() then + if Utils.isLanded(u,true) then + allInside = false + break + end + + local pos = u:getPoint() + local dist = mist.utils.get2DDist(point, pos) + if dist > range then + allInside = false + break + end + end + end + + if allInside then + firstWP.complete = true + self.mission:pushMessageToPlayers(firstWP.zone.name..' reached') + if nextWP then + self.mission:pushMessageToPlayers('Next point: '..nextWP.zone.name) + end + end + else + self.isComplete = true + return true + end + end + end + end + + function ObjFlyToZoneSequence:checkFail() + if not self.isComplete and not self.isFailed then + if self.param.failZones[1] then + for _,zn in ipairs(self.param.failZones[1]) do + if zn.side ~= 1 then + self.isFailed = true + break + end + end + end + + if self.param.failZones[2] then + for _,zn in ipairs(self.param.failZones[2]) do + if zn.side ~= 2 then + self.isFailed = true + break + end + end + end + end + end +end + +-----------------[[ END OF Objectives/ObjFlyToZoneSequence.lua ]]----------------- + + + +-----------------[[ Objectives/ObjProtectMission.lua ]]----------------- + +ObjProtectMission = Objective:new(Objective.types.protect) +do + ObjProtectMission.requiredParams = { + ['mis'] = true + } + + function ObjProtectMission:getText() + local msg = 'Prevent enemy aircraft from interfering with '..self.param.mis:getMissionName()..' mission.' + + if self.param.mis.info and self.param.mis.info.targetzone then + msg = msg..'\n Target zone: '..self.param.mis.info.targetzone.name + end + + msg = msg..'\n Protect players: ' + for i,v in pairs(self.param.mis.players) do + msg = msg..'\n '..i + end + + msg = msg..'\n Mission success depends on '..self.param.mis:getMissionName()..' mission success.' + return msg + end + + function ObjProtectMission:update() + if not self.isComplete and not self.isFailed then + if self.param.mis.state == Mission.states.failed then + self.isFailed = true + self.mission.failureReason = "Failed to protect players of "..self.param.mis.name.." mission." + end + + if self.param.mis.state == Mission.states.completed then + self.isComplete = true + end + end + end + + function ObjProtectMission:checkFail() + if not self.isComplete and not self.isFailed then + if self.param.mis.state == Mission.states.failed then + self.isFailed = true + end + + if self.param.mis.state == Mission.states.completed then + if self.mission.state == Mission.states.new or + self.mission.state == Mission.states.preping or + self.mission.state == Mission.states.comencing then + + self.isFailed = true + end + end + end + end +end + +-----------------[[ END OF Objectives/ObjProtectMission.lua ]]----------------- + + + +-----------------[[ Objectives/ObjReconZone.lua ]]----------------- + +ObjReconZone = Objective:new(Objective.types.recon_zone) +do + ObjReconZone.requiredParams = { + ['target'] = true, + ['failZones'] = true + } + + function ObjReconZone:getText() + local msg = 'Conduct a recon mission on '..self.param.target.name..' and return with data to a friendly zone.' + + return msg + end + + function ObjReconZone:update() + if not self.isComplete and not self.isFailed then + if self.param.failZones[1] then + for _,zn in ipairs(self.param.failZones[1]) do + if zn.side ~= 1 then + self.isFailed = true + self.mission.failureReason = zn.name..' is no longer controlled by the enemy.' + break + end + end + end + + if self.param.failZones[2] then + for _,zn in ipairs(self.param.failZones[2]) do + if zn.side ~= 2 then + self.isFailed = true + break + end + end + end + + if not self.isFailed then + if self.param.reconData then + self.isComplete = true + return true + end + end + end + end + + function ObjReconZone:checkFail() + if not self.isComplete and not self.isFailed then + if self.param.failZones[1] then + for _,zn in ipairs(self.param.failZones[1]) do + if zn.side ~= 1 then + self.isFailed = true + break + end + end + end + + if self.param.failZones[2] then + for _,zn in ipairs(self.param.failZones[2]) do + if zn.side ~= 2 then + self.isFailed = true + break + end + end + end + end + end +end + +-----------------[[ END OF Objectives/ObjReconZone.lua ]]----------------- + + + +-----------------[[ Objectives/ObjSupplyZone.lua ]]----------------- + +ObjSupplyZone = Objective:new(Objective.types.supply) +do + ObjSupplyZone.requiredParams = { + ['amount']=true, + ['delivered']=true, + ['tgtzone']=true + } + + function ObjSupplyZone:getText() + local msg = 'Deliver '..self.param.amount..' to '..self.param.tgtzone.name..': ' + msg = msg..'\n Progress: '..self.param.delivered..'/'..self.param.amount + return msg + end + + function ObjSupplyZone:update() + if not self.isComplete and not self.isFailed then + if self.param.delivered >= self.param.amount then + self.isComplete = true + return true + end + + local zn = self.param.tgtzone + if zn.side ~= 2 then + self.isFailed = true + self.mission.failureReason = zn.name..' was lost.' + return true + end + end + end + + function ObjSupplyZone:checkFail() + if not self.isComplete and not self.isFailed then + local zn = self.param.tgtzone + if zn.side ~= 2 then + self.isFailed = true + return true + end + end + end +end + +-----------------[[ END OF Objectives/ObjSupplyZone.lua ]]----------------- + + + +-----------------[[ Objectives/ObjExtractSquad.lua ]]----------------- + +ObjExtractSquad = Objective:new(Objective.types.extract_squad) +do + ObjExtractSquad.requiredParams = { + ['target']=true, + ['loadedBy']=false, + ['lastUpdate']= true + } + + function ObjExtractSquad:getText() + local infName = PlayerLogistics.getInfantryName(self.param.target.data.type) + local msg = 'Extract '..infName..' '..self.param.target.name..'\n' + + if not self.param.loadedBy then + local gr = Group.getByName(self.param.target.name) + if gr and gr:getSize()>0 then + local point = gr:getUnit(1):getPoint() + + local lat,lon,alt = coord.LOtoLL(point) + local mgrs = coord.LLtoMGRS(coord.LOtoLL(point)) + msg = msg..'\n DDM: '.. mist.tostringLL(lat,lon,3) + msg = msg..'\n DMS: '.. mist.tostringLL(lat,lon,2,true) + msg = msg..'\n MGRS: '.. mist.tostringMGRS(mgrs, 5) + msg = msg..'\n Altitude: '..math.floor(alt)..'m'..' | '..math.floor(alt*3.280839895)..'ft' + end + end + + return msg + end + + function ObjExtractSquad:update() + if not self.isComplete and not self.isFailed then + + if self.param.loadedBy then + self.isComplete = true + return true + else + local target = self.param.target + + local gr = Group.getByName(target.name) + if not gr or gr:getSize()==0 then + self.isFailed = true + self.mission.failureReason = 'Squad was not rescued in time, and went MIA.' + return true + end + end + + local updateFrequency = 5 -- seconds + local shouldUpdateMsg = (timer.getAbsTime() - self.param.lastUpdate) > updateFrequency + if shouldUpdateMsg then + for name, unit in pairs(self.mission.players) do + if unit and unit:isExist() then + local gr = Group.getByName(self.param.target.name) + local un = gr:getUnit(1) + local dist = mist.utils.get3DDist(unit:getPoint(), un:getPoint()) + local m = 'Distance: ' + if dist>1000 then + local dstkm = string.format('%.2f',dist/1000) + local dstnm = string.format('%.2f',dist/1852) + + m = m..dstkm..'km | '..dstnm..'nm' + else + local dstft = math.floor(dist/0.3048) + m = m..math.floor(dist)..'m | '..dstft..'ft' + end + + m = m..'\nBearing: '..math.floor(Utils.getBearing(unit:getPoint(), un:getPoint())) + trigger.action.outTextForUnit(unit:getID(), m, updateFrequency) + end + end + + self.param.lastUpdate = timer.getAbsTime() + end + end + end + + function ObjExtractSquad:checkFail() + if not self.isComplete and not self.isFailed then + local target = self.param.target + + local gr = Group.getByName(target.name) + if not gr or not gr:isExist() or gr:getSize()==0 then + self.isFailed = true + return true + end + end + end +end + +-----------------[[ END OF Objectives/ObjExtractSquad.lua ]]----------------- + + + +-----------------[[ Objectives/ObjExtractPilot.lua ]]----------------- + +ObjExtractPilot = Objective:new(Objective.types.extract_pilot) +do + ObjExtractPilot.requiredParams = { + ['target']=true, + ['loadedBy']=false, + ['lastUpdate']= true + } + + function ObjExtractPilot:getText() + local msg = 'Rescue '..self.param.target.name..'\n' + + if not self.param.loadedBy then + + if self.param.target.pilot:isExist() and + self.param.target.pilot:getSize() > 0 and + self.param.target.pilot:getUnit(1):isExist() then + + local point = self.param.target.pilot:getUnit(1):getPoint() + + local lat,lon,alt = coord.LOtoLL(point) + local mgrs = coord.LLtoMGRS(coord.LOtoLL(point)) + msg = msg..'\n DDM: '.. mist.tostringLL(lat,lon,3) + msg = msg..'\n DMS: '.. mist.tostringLL(lat,lon,2,true) + msg = msg..'\n MGRS: '.. mist.tostringMGRS(mgrs, 5) + msg = msg..'\n Altitude: '..math.floor(alt)..'m'..' | '..math.floor(alt*3.280839895)..'ft' + end + end + + return msg + end + + function ObjExtractPilot:update() + if not self.isComplete and not self.isFailed then + + if self.param.loadedBy then + self.isComplete = true + return true + else + if not self.param.target.pilot:isExist() or self.param.target.remainingTime <= 0 then + self.isFailed = true + self.mission.failureReason = 'Pilot was not rescued in time, and went MIA.' + return true + end + end + + local updateFrequency = 5 -- seconds + local shouldUpdateMsg = (timer.getAbsTime() - self.param.lastUpdate) > updateFrequency + if shouldUpdateMsg then + for name, unit in pairs(self.mission.players) do + if unit and unit:isExist() then + local gr = Group.getByName(self.param.target.name) + if gr and gr:getSize() > 0 then + local un = gr:getUnit(1) + if un then + local dist = mist.utils.get3DDist(unit:getPoint(), un:getPoint()) + local m = 'Distance: ' + if dist>1000 then + local dstkm = string.format('%.2f',dist/1000) + local dstnm = string.format('%.2f',dist/1852) + + m = m..dstkm..'km | '..dstnm..'nm' + else + local dstft = math.floor(dist/0.3048) + m = m..math.floor(dist)..'m | '..dstft..'ft' + end + + m = m..'\nBearing: '..math.floor(Utils.getBearing(unit:getPoint(), un:getPoint())) + trigger.action.outTextForUnit(unit:getID(), m, updateFrequency) + end + end + end + end + + self.param.lastUpdate = timer.getAbsTime() + end + end + end + + function ObjExtractPilot:checkFail() + if not self.isComplete and not self.isFailed then + if not self.param.target.pilot:isExist() or self.param.target.remainingTime <= 0 then + self.isFailed = true + return true + end + end + end +end + +-----------------[[ END OF Objectives/ObjExtractPilot.lua ]]----------------- + + + +-----------------[[ Objectives/ObjUnloadExtractedPilotOrSquad.lua ]]----------------- + +ObjUnloadExtractedPilotOrSquad = Objective:new(Objective.types.unloaded_pilot_or_squad) +do + ObjUnloadExtractedPilotOrSquad.requiredParams = { + ['targetZone']=false, + ['extractObjective']=true, + ['unloadedAt']=false + } + + function ObjUnloadExtractedPilotOrSquad:getText() + local msg = 'Drop off personnel ' + if self.param.targetZone then + msg = msg..'at '..self.param.targetZone.name..'\n' + else + msg = msg..'at a friendly zone\n' + end + + return msg + end + + function ObjUnloadExtractedPilotOrSquad:update() + if not self.isComplete and not self.isFailed then + + if self.param.extractObjective.isComplete and self.param.unloadedAt then + if self.param.targetZone then + if self.param.unloadedAt == self.param.targetZone.name then + self.isComplete = true + return true + else + self.isFailed = true + self.mission.failureReason = 'Personnel dropped off at wrong zone.' + return true + end + else + self.isComplete = true + return true + end + end + + if self.param.extractObjective.isFailed then + self.isFailed = true + return true + end + + if self.param.targetZone and self.param.targetZone.side ~= 2 then + self.isFailed = true + self.mission.failureReason = self.param.targetZone.name..' was lost.' + return true + end + end + end + + function ObjUnloadExtractedPilotOrSquad:checkFail() + if not self.isComplete and not self.isFailed then + + if self.param.extractObjective.isFailed then + self.isFailed = true + return true + end + + if self.param.targetZone and self.param.targetZone.side ~= 2 then + self.isFailed = true + return true + end + end + end +end + +-----------------[[ END OF Objectives/ObjUnloadExtractedPilotOrSquad.lua ]]----------------- + + + +-----------------[[ Objectives/ObjPlayerCloseToZone.lua ]]----------------- + +ObjPlayerCloseToZone = Objective:new(Objective.types.player_close_to_zone) +do + ObjPlayerCloseToZone.requiredParams = { + ['target']=true, + ['range'] = true, + ['amount']= true, + ['maxAmount'] = true, + ['lastUpdate']= true + } + + function ObjPlayerCloseToZone:getText() + local msg = 'Patrol area around '..self.param.target.name + + local prg = math.floor(((self.param.maxAmount - self.param.amount)/self.param.maxAmount)*100) + msg = msg.. '\n Progress: '..prg..'%' + return msg + end + + function ObjPlayerCloseToZone:update() + if not self.isComplete and not self.isFailed then + + if self.param.target.side ~= 2 then + self.isFailed = true + self.mission.failureReason = self.param.target.name..' was lost.' + return true + end + + local plycount = Utils.getTableSize(self.mission.players) + if plycount == 0 then plycount = 1 end + local updateFrequency = 5 -- seconds + local shouldUpdateMsg = (timer.getAbsTime() - self.param.lastUpdate) > updateFrequency + for name, unit in pairs(self.mission.players) do + if unit and unit:isExist() and Utils.isInAir(unit) then + local dist = mist.utils.get2DDist(unit:getPoint(), self.param.target.zone.point) + if dist < self.param.range then + self.param.amount = self.param.amount - (1/plycount) + + if shouldUpdateMsg then + local prg = string.format('%.1f',((self.param.maxAmount - self.param.amount)/self.param.maxAmount)*100) + trigger.action.outTextForUnit(unit:getID(), '['..self.param.target.name..'] Progress: '..prg..'%', updateFrequency) + end + end + end + end + + if shouldUpdateMsg then + self.param.lastUpdate = timer.getAbsTime() + end + + if self.param.amount <= 0 then + self.isComplete = true + for name, unit in pairs(self.mission.players) do + if unit and unit:isExist() and Utils.isInAir(unit) then + trigger.action.outTextForUnit(unit:getID(), '['..self.param.target.name..'] Complete', updateFrequency) + end + end + return true + end + end + end + + function ObjPlayerCloseToZone:checkFail() + if not self.isComplete and not self.isFailed then + if self.param.target.side ~= 2 then + self.isFailed = true + end + end + end +end + +-----------------[[ END OF Objectives/ObjPlayerCloseToZone.lua ]]----------------- + + + +-----------------[[ Objectives/ObjDeploySquad.lua ]]----------------- + +ObjDeploySquad = Objective:new(Objective.types.deploy_squad) +do + ObjDeploySquad.requiredParams = { + ['squadType']=true, + ['targetZone']=true, + ['requiredZoneSide']=true, + ['unloadedType']=false, + ['unloadedAt']=false + } + + function ObjDeploySquad:getText() + local infName = PlayerLogistics.getInfantryName(self.param.squadType) + local msg = 'Deploy '..infName..' at '..self.param.targetZone.name + return msg + end + + function ObjDeploySquad:update() + if not self.isComplete and not self.isFailed then + + if self.param.unloadedType and self.param.unloadedAt then + if self.param.targetZone.name == self.param.unloadedAt then + if self.param.squadType == self.param.unloadedType then + self.isComplete = true + return true + end + end + end + + if self.param.targetZone.side ~= self.param.requiredZoneSide then + self.isFailed = true + + local side = '' + if self.param.requiredZoneSide == 0 then side = 'neutral' + elseif self.param.requiredZoneSide == 1 then side = 'controlled by Red' + elseif self.param.requiredZoneSide == 2 then side = 'controlled by Blue' + end + + self.mission.failureReason = self.param.targetZone.name..' is no longer '..side + return true + end + end + end + + function ObjDeploySquad:checkFail() + if not self.isComplete and not self.isFailed then + if self.param.targetZone.side ~= self.param.requiredZoneSide then + self.isFailed = true + return true + end + end + end +end + +-----------------[[ END OF Objectives/ObjDeploySquad.lua ]]----------------- + + + +-----------------[[ Missions/Mission.lua ]]----------------- + +Mission = {} +do + Mission.states = { + new = 'new', -- mission was just generated and is listed publicly + preping = 'preping', -- mission was accepted by a player, was delisted, and player recieved a join code that can be shared + comencing = 'comencing', -- a player that is subscribed to the mission has taken off, join code is invalidated + active = 'active', -- all players subscribed to the mission have taken off, objective can now be accomplished + completed = 'completed', -- mission objective was completed, players need to land to claim rewards + failed = 'failed' -- mission lost all players OR mission objective no longer possible to accomplish + } + + --[[ + new -> preping -> comencing -> active -> completed + | | | |-> failed + | | |->failed + | |->failed + |->failed + --]] + + Mission.types = { + cap_easy = 'cap_easy', -- fly over zn A-B-A-B-A-B OR destroy few enemy aircraft + cap_medium = 'cap_medium', -- fly over zn A-B-A-B-A-B AND destroy few enemy aircraft -- push list of aircraft within range of target zones + tarcap = 'tarcap', -- protect other mission, air kills increase reward + --tarcap = 'tarcap', -- update target mission list after all other missions are in + + cas_easy = 'cas_easy', -- destroy small amount of ground units + cas_medium = 'cas_medium', -- destroy large amount of ground units + cas_hard = 'cas_hard', -- destroy all defenses at zone A + bai = 'bai', -- destroy any enemy convoy - show "last" location of convoi (BRA or LatLon) update every 30 seconds + + sead = 'sead', -- destroy any SAM TR or SAM SR at zone A + dead = 'dead', -- destroy all SAM TR or SAM SR, or IR Guided SAM at zone A + + strike_veryeasy = 'strike_veryeasy', -- destroy 1 building + strike_easy = 'strike_easy', -- destroy any structure at zone A + strike_medium = 'strike_medium',-- destroy specific structure at zone A - show LatLon and Alt in mission description + strike_hard = 'strike_hard', -- destroy all structures at zone A and turn it neutral + deep_strike = 'deep_strike', -- destroy specific structure taken from strike queue - show LatLon and Alt in mission description + + anti_runway = 'anti_runway', -- drop at least X anti runway bombs on runway zone (if player unit launches correct weapon, track, if agl>10m check if in zone, tally), define list of runway zones somewhere + + supply_easy = 'supply_easy', -- transfer resources to zone A(low supply) + supply_hard = 'supply_hard', -- transfer resources to zone A(low supply), high resource number + escort = 'escort', -- follow and protect friendly convoy until they get to target OR 10 minutes pass + csar = 'csar', -- extract specific pilot to friendly zone, track friendly pilots ejected + recon = 'recon', -- conduct recon + extraction = 'extraction', -- extract a deployed squad to friendly zone, generate mission if squad has extractionReady state + deploy_squad = 'deploy_squad', -- deploy squad to zone + } + + Mission.completion_type = { + any = 'any', + all = 'all' + } + + function Mission:new(id, type) + local expire = math.random(60*15, 60*30) + + local obj = { + missionID = id, + type = type, + name = '', + description = '', + failureReason = nil, + state = Mission.states.new, + expireTime = expire, + lastStateTime = timer.getAbsTime(), + objectives = {}, + completionType = Mission.completion_type.any, + rewards = {}, + players = {}, + info = {} + } + + setmetatable(obj, self) + self.__index = self + + if obj.getExpireTime then obj.expireTime = obj:getExpireTime() end + if obj.getMissionName then obj.name = obj:getMissionName() end + if obj.generateObjectives then obj:generateObjectives() end + if obj.generateRewards then obj:generateRewards() end + + return obj + end + + function Mission:updateState(newstate) + env.info('Mission - code'..self.missionID..' updateState state changed from '..self.state..' to '..newstate) + self.state = newstate + self.lastStateTime = timer.getAbsTime() + if self.state == self.states.preping then + if self.info.targetzone then + MissionTargetRegistry.addZone(self.info.targetzone.name) + end + elseif self.state == self.states.completed or self.state == self.states.failed then + if self.info.targetzone then + MissionTargetRegistry.removeZone(self.info.targetzone.name) + end + end + end + + function Mission:pushMessageToPlayer(player, msg, duration) + if not duration then + duration = 10 + end + + for name,un in pairs(self.players) do + if name == player and un and un:isExist() then + trigger.action.outTextForUnit(un:getID(), msg, duration) + break + end + end + end + + function Mission:pushMessageToPlayers(msg, duration) + if not duration then + duration = 10 + end + + for _,un in pairs(self.players) do + if un and un:isExist() then + trigger.action.outTextForUnit(un:getID(), msg, duration) + end + end + end + + function Mission:pushSoundToPlayers(sound) + for _,un in pairs(self.players) do + if un and un:isExist() then + --trigger.action.outSoundForUnit(un:getID(), sound) -- does not work correctly in multiplayer + trigger.action.outSoundForGroup(un:getGroup():getID(), sound) + end + end + end + + function Mission:removePlayer(player) + for pl,un in pairs(self.players) do + if pl == player then + self.players[pl] = nil + break + end + end + end + + function Mission:isInstantReward() + return false + end + + function Mission:hasPlayers() + return Utils.getTableSize(self.players) > 0 + end + + function Mission:getPlayerUnit(player) + return self.players[player] + end + + function Mission:addPlayer(player, unit) + self.players[player] = unit + end + + function Mission:checkFailConditions() + if self.state == Mission.states.active then return end + + for _,obj in ipairs(self.objectives) do + local shouldBreak = obj:checkFail() + + if shouldBreak then break end + end + end + + function Mission:updateObjectives() + if self.state ~= self.states.active then return end + + for _,obj in ipairs(self.objectives) do + local shouldBreak = obj:update() + + if obj.isFailed and self.objectiveFailedCallback then self:objectiveFailedCallback(obj) end + if not obj.isFailed and obj.isComplete and self.objectiveCompletedCallback then self:objectiveCompletedCallback(obj) end + + if shouldBreak then break end + end + end + + function Mission:updateIsFailed() + self:checkFailConditions() + + local allFailed = true + for _,obj in ipairs(self.objectives) do + if self.state == Mission.states.new then + if obj.isFailed then + self:updateState(Mission.states.failed) + env.info("Mission code"..self.missionID.." objective cancelled:\n"..obj:getText()) + break + end + end + + if self.completionType == Mission.completion_type.all then + if obj.isFailed then + self:updateState(Mission.states.failed) + env.info("Mission code"..self.missionID.." (all) objective failed:\n"..obj:getText()) + break + end + end + + if not obj.isFailed then + allFailed = false + end + end + + if self.completionType == Mission.completion_type.any and allFailed then + self:updateState(Mission.states.failed) + env.info("Mission code"..self.missionID.." all objectives failed") + end + end + + function Mission:updateIsCompleted() + if self.completionType == self.completion_type.any then + for _,obj in ipairs(self.objectives) do + if obj.isComplete then + self:updateState(self.states.completed) + env.info("Mission code"..self.missionID.." (any) objective completed:\n"..obj:getText()) + break + end + end + elseif self.completionType == self.completion_type.all then + local allComplete = true + for _,obj in ipairs(self.objectives) do + if not obj.isComplete then + allComplete = false + break + end + end + + if allComplete then + self:updateState(self.states.completed) + env.info("Mission code"..self.missionID.." all objectives complete") + end + end + end + + function Mission:tallyWeapon(weapon) + for _,obj in ipairs(self.objectives) do + if not obj.isComplete and not obj.isFailed then + if obj.type == ObjBombInsideZone:getType() then + for i,v in ipairs(obj.param.targetZone:getRunwayZones()) do + if Utils.isInZone(weapon, v.name) then + if obj.param.dropped < obj.param.max then + obj.param.dropped = obj.param.dropped + 1 + if obj.param.dropped > obj.param.required then + for _,rew in ipairs(self.rewards) do + if obj.param.bonus[rew.type] then + rew.amount = rew.amount + obj.param.bonus[rew.type] + + if rew.type == PlayerTracker.statTypes.xp then + self:pushMessageToPlayers("Bonus: + "..obj.param.bonus[rew.type]..' XP') + end + end + end + end + end + break + end + end + end + end + end + end + + function Mission:tallyKill(kill) + for _,obj in ipairs(self.objectives) do + if not obj.isComplete and not obj.isFailed then + if obj.type == ObjDestroyUnitsWithAttribute:getType() then + for _,a in ipairs(obj.param.attr) do + if kill:hasAttribute(a) then + obj.param.killed = obj.param.killed + 1 + break + elseif a == 'Buildings' and ZoneCommand and ZoneCommand.staticRegistry[kill:getName()] then + obj.param.killed = obj.param.killed + 1 + break + end + end + elseif obj.type == ObjDestroyStructure:getType() then + if obj.param.target.name == kill:getName() then + obj.param.killed = true + end + elseif obj.type == ObjDestroyGroup:getType() then + if kill.getName then + if obj.param.targetUnitNames[kill:getName()] ~= nil then + obj.param.targetUnitNames[kill:getName()] = true + end + end + elseif obj.type == ObjAirKillBonus:getType() then + for _,a in ipairs(obj.param.attr) do + if kill:hasAttribute(a) then + for _,rew in ipairs(self.rewards) do + if obj.param.bonus[rew.type] then + rew.amount = rew.amount + obj.param.bonus[rew.type] + obj.param.count = obj.param.count + 1 + if rew.type == PlayerTracker.statTypes.xp then + self:pushMessageToPlayers("Reward increased: + "..obj.param.bonus[rew.type]..' XP') + end + end + end + break + elseif a == 'Buildings' and ZoneCommand and ZoneCommand.staticRegistry[kill:getName()] then + for _,rew in ipairs(self.rewards) do + if obj.param.bonus[rew.type] then + rew.amount = rew.amount + obj.param.bonus[rew.type] + obj.param.count = obj.param.count + 1 + + if rew.type == PlayerTracker.statTypes.xp then + self:pushMessageToPlayers("Reward increased: + "..obj.param.bonus[rew.type]..' XP') + end + end + end + break + end + end + elseif obj.type == ObjDestroyUnitsWithAttributeAtZone:getType() then + local zn = obj.param.tgtzone + if zn then + local validzone = false + if Utils.isInZone(kill, zn.name) then + validzone = true + else + for nm,_ in pairs(zn.built) do + local gr = Group.getByName(nm) + if gr then + for _,un in ipairs(gr:getUnits()) do + if un:getID() == kill:getID() then + validzone = true + break + end + end + end + + if validzone then break end + end + end + + if validzone then + for _,a in ipairs(obj.param.attr) do + if kill:hasAttribute(a) then + obj.param.killed = obj.param.killed + 1 + break + elseif a == 'Buildings' and ZoneCommand and ZoneCommand.staticRegistry[kill:getName()] then + obj.param.killed = obj.param.killed + 1 + break + end + end + end + end + end + end + end + end + + function Mission:isUnitTypeAllowed(unit) + return true + end + + function Mission:tallySupplies(amount, zonename) + for _,obj in ipairs(self.objectives) do + if not obj.isComplete and not obj.isFailed then + if obj.type == ObjSupplyZone:getType() then + if obj.param.tgtzone.name == zonename then + obj.param.delivered = obj.param.delivered + amount + end + end + end + end + end + + function Mission:tallyLoadPilot(player, pilot) + for _,obj in ipairs(self.objectives) do + if not obj.isComplete and not obj.isFailed then + if obj.type == ObjExtractPilot:getType() then + if obj.param.target.name == pilot.name then + obj.param.loadedBy = player + end + end + end + end + end + + function Mission:tallyUnloadPilot(player, zonename) + for _,obj in ipairs(self.objectives) do + if not obj.isComplete and not obj.isFailed then + if obj.type == ObjUnloadExtractedPilotOrSquad:getType() then + if obj.param.extractObjective.param.loadedBy == player then + obj.param.unloadedAt = zonename + end + end + end + end + end + + function Mission:tallyRecon(player, targetzone, analyzezonename) + for _,obj in ipairs(self.objectives) do + if not obj.isComplete and not obj.isFailed then + if obj.type == ObjReconZone:getType() then + if obj.param.target.name == targetzone then + obj.param.reconData = targetzone + end + end + end + end + end + + function Mission:tallyLoadSquad(player, squad) + for _,obj in ipairs(self.objectives) do + if not obj.isComplete and not obj.isFailed then + if obj.type == ObjExtractSquad:getType() then + if obj.param.target.name == squad.name then + obj.param.loadedBy = player + end + end + end + end + end + + function Mission:tallyUnloadSquad(player, zonename, unloadedType) + for _,obj in ipairs(self.objectives) do + if not obj.isComplete and not obj.isFailed then + if obj.type == ObjUnloadExtractedPilotOrSquad:getType() then + if obj.param.extractObjective.param.loadedBy == player and unloadedType == PlayerLogistics.infantryTypes.extractable then + obj.param.unloadedAt = zonename + end + elseif obj.type == ObjDeploySquad:getType() then + obj.param.unloadedType = unloadedType + obj.param.unloadedAt = zonename + end + end + end + end + + function Mission:getBriefDescription() + local msg = '~~~~~'..self.name..' ['..self.missionID..']~~~~~\n'..self.description..'\n' + + msg = msg..' Reward:' + + for _,r in ipairs(self.rewards) do + msg = msg..' ['..r.type..': '..r.amount..']' + end + + return msg + end + + function Mission:generateRewards() + if not self.type then return end + + local rewardDef = RewardDefinitions.missions[self.type] + + self.rewards = {} + table.insert(self.rewards, { + type = PlayerTracker.statTypes.xp, + amount = math.random(rewardDef.xp.low,rewardDef.xp.high)*50 + }) + end + + function Mission:getDetailedDescription() + local msg = '['..self.name..']' + + if self.state == Mission.states.comencing or self.state == Mission.states.preping or (not Config.restrictMissionAcceptance) then + msg = msg..'\nJoin code ['..self.missionID..']' + end + + msg = msg..'\nReward:' + + for _,r in ipairs(self.rewards) do + msg = msg..' ['..r.type..': '..r.amount..']' + end + msg = msg..'\n' + + if #self.objectives>1 then + msg = msg..'\nObjectives: ' + if self.completionType == Mission.completion_type.all then + msg = msg..'(Complete ALL)\n' + elseif self.completionType == Mission.completion_type.any then + msg = msg..'(Complete ONE)\n' + end + elseif #self.objectives==1 then + msg = msg..'\nObjective: \n' + end + + for i,v in ipairs(self.objectives) do + local obj = v:getText() + if v.isComplete then + obj = '[✓]'..obj + elseif v.isFailed then + obj = '[X]'..obj + else + obj = '[ ]'..obj + end + + msg = msg..'\n'..obj..'\n' + end + + msg = msg..'\nPlayers:' + for i,_ in pairs(self.players) do + msg = msg..'\n '..i + end + + return msg + end +end + +-----------------[[ END OF Missions/Mission.lua ]]----------------- + + + +-----------------[[ Missions/CAP_Easy.lua ]]----------------- + +CAP_Easy = Mission:new() +do + function CAP_Easy.canCreate() + local zoneNum = 0 + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 2 and zone.distToFront == 0 then + zoneNum = zoneNum + 1 + end + + if zoneNum >= 2 then return true end + end + end + + function CAP_Easy:getMissionName() + return 'CAP' + end + + function CAP_Easy:isUnitTypeAllowed(unit) + return unit:hasAttribute('Planes') + end + + function CAP_Easy:generateObjectives() + self.completionType = Mission.completion_type.any + local description = '' + local viableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 2 and zone.distToFront == 0 then + table.insert(viableZones, zone) + end + end + + if #viableZones >= 2 then + local choice1 = math.random(1,#viableZones) + local zn1 = viableZones[choice1] + + local patrol1 = ObjPlayerCloseToZone:new() + patrol1:initialize(self, { + target = zn1, + range = 20000, + amount = 15*60, + maxAmount = 15*60, + lastUpdate = 0 + }) + + table.insert(self.objectives, patrol1) + description = description..' Patrol airspace near '..zn1.name..'\n OR\n' + end + + local kills = ObjDestroyUnitsWithAttribute:new() + kills:initialize(self, { + attr = {'Planes', 'Helicopters'}, + amount = math.random(2,4), + killed = 0 + }) + + table.insert(self.objectives, kills) + description = description..' Kill '..kills.param.amount..' aircraft' + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/CAP_Easy.lua ]]----------------- + + + +-----------------[[ Missions/CAP_Medium.lua ]]----------------- + +CAP_Medium = Mission:new() +do + function CAP_Medium.canCreate() + local zoneNum = 0 + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 2 and zone.distToFront == 0 then + zoneNum = zoneNum + 1 + end + + if zoneNum >= 2 then return true end + end + end + + function CAP_Medium:getMissionName() + return 'CAP' + end + + function CAP_Medium:isUnitTypeAllowed(unit) + return unit:hasAttribute('Planes') + end + + function CAP_Medium:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + local viableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 2 and zone.distToFront == 0 then + table.insert(viableZones, zone) + end + end + + if #viableZones >= 2 then + local choice1 = math.random(1,#viableZones) + local zn1 = viableZones[choice1] + table.remove(viableZones,choice1) + local choice2 = math.random(1,#viableZones) + local zn2 = viableZones[choice2] + + local patrol1 = ObjPlayerCloseToZone:new() + patrol1:initialize(self, { + target = zn1, + range = 20000, + amount = 10*60, + maxAmount = 10*60, + lastUpdate = 0 + }) + + table.insert(self.objectives, patrol1) + + local patrol2 = ObjPlayerCloseToZone:new() + patrol2:initialize(self, { + target = zn2, + range = 20000, + amount = 10*60, + maxAmount = 10*60, + lastUpdate = 0 + }) + + table.insert(self.objectives, patrol2) + description = description..' Patrol airspace near '..zn1.name..' and '..zn2.name..'\n' + + local rewardDef = RewardDefinitions.missions[self.type] + + local kills = ObjAirKillBonus:new() + kills:initialize(self, { + attr = {'Planes', 'Helicopters'}, + bonus = { + [PlayerTracker.statTypes.xp] = rewardDef.xp.boost + }, + count = 0, + linkedObjectives = {patrol1, patrol2} + }) + + table.insert(self.objectives, kills) + description = description..' Aircraft kills increase reward' + end + + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/CAP_Medium.lua ]]----------------- + + + +-----------------[[ Missions/CAS_Easy.lua ]]----------------- + +CAS_Easy = Mission:new() +do + function CAS_Easy.canCreate() + return true + end + + function CAS_Easy:getMissionName() + return 'CAS' + end + + function CAS_Easy:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + + local kills = ObjDestroyUnitsWithAttribute:new() + kills:initialize(self, { + attr = {'Ground Units'}, + amount = math.random(3,6), + killed = 0 + }) + + table.insert(self.objectives, kills) + description = description..' Destroy '..kills.param.amount..' Ground Units' + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/CAS_Easy.lua ]]----------------- + + + +-----------------[[ Missions/CAS_Medium.lua ]]----------------- + +CAS_Medium = Mission:new() +do + function CAS_Medium.canCreate() + return true + end + + function CAS_Medium:getMissionName() + return 'CAS' + end + + function CAS_Medium:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + + local kills = ObjDestroyUnitsWithAttribute:new() + kills:initialize(self, { + attr = {'Ground Units'}, + amount = math.random(8,12), + killed = 0 + }) + + table.insert(self.objectives, kills) + description = description..' Destroy '..kills.param.amount..' Ground Units' + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/CAS_Medium.lua ]]----------------- + + + +-----------------[[ Missions/CAS_Hard.lua ]]----------------- + +CAS_Hard = Mission:new() +do + function CAS_Hard.canCreate() + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront and zone.distToFront <=1 and zone:hasUnitWithAttributeOnSide({"Ground Units"}, 1, 6) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + return true + end + end + end + end + + function CAS_Hard:getMissionName() + return 'CAS' + end + + function CAS_Hard:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + local viableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront == 0 and zone:hasUnitWithAttributeOnSide({"Ground Units"}, 1, 6) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(viableZones, zone) + end + end + end + + if #viableZones == 0 then + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront == 1 and zone:hasUnitWithAttributeOnSide({"Ground Units"}, 1, 6) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(viableZones, zone) + end + end + end + end + + if #viableZones > 0 then + local choice = math.random(1,#viableZones) + local zn = viableZones[choice] + + local kill = ObjDestroyUnitsWithAttributeAtZone:new() + kill:initialize(self, { + attr = {"Ground Units"}, + amount = 1, + killed = 0, + tgtzone = zn + }) + table.insert(self.objectives, kill) + + local clear = ObjClearZoneOfUnitsWithAttribute:new() + clear:initialize(self, { + attr = {"Ground Units"}, + tgtzone = zn + }) + table.insert(self.objectives, clear) + + description = description..' Clear '..zn.name..' of ground units' + self.info = { + targetzone = zn + } + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/CAS_Hard.lua ]]----------------- + + + +-----------------[[ Missions/SEAD.lua ]]----------------- + +SEAD = Mission:new() +do + function SEAD.canCreate() + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront and zone.distToFront <=1 and zone:hasSAMRadarOnSide(1) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + return true + end + end + end + end + + function SEAD:getMissionName() + return 'SEAD' + end + + function SEAD:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + local viableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront == 0 and zone:hasSAMRadarOnSide(1) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(viableZones, zone) + end + end + end + + if #viableZones == 0 then + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront == 1 and zone:hasSAMRadarOnSide(1) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(viableZones, zone) + end + end + end + end + + if #viableZones > 0 then + local choice = math.random(1,#viableZones) + local zn = viableZones[choice] + + local kill = ObjDestroyUnitsWithAttributeAtZone:new() + kill:initialize(self, { + attr = {'SAM SR','SAM TR'}, + amount = 1, + killed = 0, + tgtzone = zn + }) + + table.insert(self.objectives, kill) + description = description..' Destroy '..kill.param.amount..' Search Radar or Track Radar at '..zn.name + self.info = { + targetzone = zn + } + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/SEAD.lua ]]----------------- + + + +-----------------[[ Missions/DEAD.lua ]]----------------- + +DEAD = Mission:new() +do + function DEAD.canCreate() + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront and zone.distToFront <=1 and zone:hasUnitWithAttributeOnSide({"Air Defence"}, 1, 4) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + return true + end + end + end + end + + function DEAD:getMissionName() + return 'DEAD' + end + + function DEAD:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + local viableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront <=1 and zone:hasUnitWithAttributeOnSide({"Air Defence"}, 1, 4) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(viableZones, zone) + end + end + end + + if #viableZones > 0 then + local choice = math.random(1,#viableZones) + local zn = viableZones[choice] + + local kill = ObjDestroyUnitsWithAttributeAtZone:new() + kill:initialize(self, { + attr = {"Air Defence"}, + amount = 1, + killed = 0, + tgtzone = zn + }) + table.insert(self.objectives, kill) + + local clear = ObjClearZoneOfUnitsWithAttribute:new() + clear:initialize(self, { + attr = {"Air Defence"}, + tgtzone = zn + }) + table.insert(self.objectives, clear) + + description = description..' Clear '..zn.name..' of any Air Defenses' + self.info = { + targetzone = zn + } + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/DEAD.lua ]]----------------- + + + +-----------------[[ Missions/Supply_Easy.lua ]]----------------- + +Supply_Easy = Mission:new() +do + function Supply_Easy.canCreate() + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 2 and zone.distToFront and zone.distToFront <=1 and zone:criticalOnSupplies() then + return true + end + end + end + + function Supply_Easy:getMissionName() + return "Supply delivery" + end + + function Supply_Easy:isInstantReward() + return true + end + + function Supply_Easy:isUnitTypeAllowed(unit) + if PlayerLogistics then + local unitType = unit:getDesc()['typeName'] + return PlayerLogistics.allowedTypes[unitType] and PlayerLogistics.allowedTypes[unitType].supplies + end + end + + function Supply_Easy:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + + local viableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 2 and zone.distToFront <=1 and zone:criticalOnSupplies() then + table.insert(viableZones, zone) + end + end + + if #viableZones > 0 then + local choice = math.random(1,#viableZones) + local zn = viableZones[choice] + + local deliver = ObjSupplyZone:new() + deliver:initialize(self, { + amount = math.random(2,6)*250, + delivered = 0, + tgtzone = zn + }) + + table.insert(self.objectives, deliver) + description = description..' Deliver '..deliver.param.amount..' of supplies to '..zn.name + self.info = { + targetzone = zn + } + end + + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Supply_Easy.lua ]]----------------- + + + +-----------------[[ Missions/Supply_Hard.lua ]]----------------- + +Supply_Hard = Mission:new() +do + function Supply_Hard.canCreate() + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 2 and zone.distToFront and zone.distToFront <=1 and zone:criticalOnSupplies() then + return true + end + end + end + + function Supply_Hard:getMissionName() + return "Supply delivery" + end + + function Supply_Hard:isInstantReward() + return true + end + + function Supply_Hard:isUnitTypeAllowed(unit) + if PlayerLogistics then + local unitType = unit:getDesc()['typeName'] + return PlayerLogistics.allowedTypes[unitType] and PlayerLogistics.allowedTypes[unitType].supplies + end + end + + function Supply_Hard:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + + local viableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 2 and zone.distToFront == 0 and zone:criticalOnSupplies() then + table.insert(viableZones, zone) + end + end + + if #viableZones == 0 then + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 2 and zone.distToFront == 1 and zone:criticalOnSupplies() then + table.insert(viableZones, zone) + end + end + end + + if #viableZones > 0 then + local choice = math.random(1,#viableZones) + local zn = viableZones[choice] + + local deliver = ObjSupplyZone:new() + deliver:initialize(self, { + amount = math.random(18,24)*250, + delivered = 0, + tgtzone = zn + }) + + table.insert(self.objectives, deliver) + description = description..' Deliver '..deliver.param.amount..' of supplies to '..zn.name + self.info = { + targetzone = zn + } + end + + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Supply_Hard.lua ]]----------------- + + + +-----------------[[ Missions/Strike_VeryEasy.lua ]]----------------- + +Strike_VeryEasy = Mission:new() +do + function Strike_VeryEasy.canCreate() + return true + end + + function Strike_VeryEasy:getMissionName() + return 'Strike' + end + + function Strike_VeryEasy:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + + local kills = ObjDestroyUnitsWithAttribute:new() + kills:initialize(self, { + attr = {'Buildings'}, + amount = 1, + killed = 0 + }) + + table.insert(self.objectives, kills) + description = description..' Destroy '..kills.param.amount..' building' + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Strike_VeryEasy.lua ]]----------------- + + + +-----------------[[ Missions/Strike_Easy.lua ]]----------------- + +Strike_Easy = Mission:new() +do + function Strike_Easy.canCreate() + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront and zone.distToFront <=1 and zone:hasUnitWithAttributeOnSide({'Buildings'}, 1) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + return true + end + end + end + end + + function Strike_Easy:getMissionName() + return 'Strike' + end + + function Strike_Easy:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + local viableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront == 0 and zone:hasUnitWithAttributeOnSide({'Buildings'}, 1) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(viableZones, zone) + end + end + end + + if #viableZones == 0 then + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront == 1 and zone:hasUnitWithAttributeOnSide({'Buildings'}, 1) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(viableZones, zone) + end + end + end + end + + if #viableZones > 0 then + local choice = math.random(1,#viableZones) + local zn = viableZones[choice] + + local kill = ObjDestroyUnitsWithAttributeAtZone:new() + kill:initialize(self, { + attr = {'Buildings'}, + amount = 1, + killed = 0, + tgtzone = zn + }) + + table.insert(self.objectives, kill) + description = description..' Destroy '..kill.param.amount..' Building at '..zn.name + self.info = { + targetzone = zn + } + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Strike_Easy.lua ]]----------------- + + + +-----------------[[ Missions/Strike_Medium.lua ]]----------------- + +Strike_Medium = Mission:new() +do + function Strike_Medium.canCreate() + return MissionTargetRegistry.strikeTargetsAvailable(1, false) + end + + function Strike_Medium:getMissionName() + return 'Strike' + end + + function Strike_Medium:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + local viableZones = {} + + local tgt = MissionTargetRegistry.getRandomStrikeTarget(1, false) + + if tgt then + local chozenTarget = tgt.data + local zn = tgt.zone + + local kill = ObjDestroyStructure:new() + kill:initialize(self, { + target = chozenTarget, + tgtzone = zn, + killed = false + }) + + table.insert(self.objectives, kill) + description = description..' Destroy '..chozenTarget.display..' at '..zn.name + self.info = { + targetzone = zn + } + + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Strike_Medium.lua ]]----------------- + + + +-----------------[[ Missions/Strike_Hard.lua ]]----------------- + +Strike_Hard = Mission:new() +do + function Strike_Hard.canCreate() + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront and zone.distToFront <=1 and zone:hasUnitWithAttributeOnSide({"Buildings"}, 1, 3) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + return true + end + end + end + end + + function Strike_Hard:getMissionName() + return 'Strike' + end + + function Strike_Hard:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + local viableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront == 0 and zone:hasUnitWithAttributeOnSide({"Buildings"}, 1, 3) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(viableZones, zone) + end + end + end + + if #viableZones == 0 then + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront == 1 and zone:hasUnitWithAttributeOnSide({"Buildings"}, 1, 3) then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(viableZones, zone) + end + end + end + end + + if #viableZones > 0 then + local choice = math.random(1,#viableZones) + local zn = viableZones[choice] + + local kill = ObjDestroyUnitsWithAttributeAtZone:new() + kill:initialize(self, { + attr = {"Buildings"}, + amount = 1, + killed = 0, + tgtzone = zn + }) + + table.insert(self.objectives, kill) + + local clear = ObjClearZoneOfUnitsWithAttribute:new() + clear:initialize(self, { + attr = {"Buildings"}, + tgtzone = zn + }) + table.insert(self.objectives, clear) + + description = description..' Destroy every structure at '..zn.name + self.info = { + targetzone = zn + } + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Strike_Hard.lua ]]----------------- + + + +-----------------[[ Missions/Deep_Strike.lua ]]----------------- + +Deep_Strike = Mission:new() +do + function Deep_Strike.canCreate() + return MissionTargetRegistry.strikeTargetsAvailable(1, true) + end + + function Deep_Strike:getMissionName() + return 'Deep Strike' + end + + function Deep_Strike:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + local viableZones = {} + + local tgt = MissionTargetRegistry.getRandomStrikeTarget(1, true) + + if tgt then + local chozenTarget = tgt.data + local zn = tgt.zone + + local kill = ObjDestroyStructure:new() + kill:initialize(self, { + target = chozenTarget, + tgtzone = zn, + killed = false + }) + + table.insert(self.objectives, kill) + description = description..' Destroy '..chozenTarget.display..' at '..zn.name + self.info = { + targetzone = zn + } + + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Deep_Strike.lua ]]----------------- + + + +-----------------[[ Missions/Escort.lua ]]----------------- + +Escort = Mission:new() +do + function Escort.canCreate() + local currentTime = timer.getAbsTime() + for _,gr in pairs(DependencyManager.get("GroupMonitor").groups) do + if gr.product.side == 2 and gr.product.type == 'mission' and (gr.product.missionType == 'supply_convoy' or gr.product.missionType == 'assault') then + local z = gr.target + if z.distToFront == 0 and z.side~= 2 then + if gr.state == nil or gr.state == 'started' or (gr.state == 'enroute' and (currentTime - gr.lastStateTime < 7*60)) then + return true + end + end + end + end + end + + function Escort:getMissionName() + return "Escort convoy" + end + + function Escort:isUnitTypeAllowed(unit) + return unit:hasAttribute('Helicopters') + end + + function Escort:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + + local currentTime = timer.getAbsTime() + local viableConvoys = {} + for _,gr in pairs(DependencyManager.get("GroupMonitor").groups) do + if gr.product.side == 2 and gr.product.type == 'mission' and (gr.product.missionType == 'supply_convoy' or gr.product.missionType == 'assault') then + local z = gr.target + if z.distToFront == 0 and z.side ~= 2 then + if gr.state == nil or gr.state == 'started' or (gr.state == 'enroute' and (currentTime - gr.lastStateTime < 7*60)) then + table.insert(viableConvoys, gr) + end + end + end + end + + if #viableConvoys > 0 then + local choice = math.random(1,#viableConvoys) + local convoy = viableConvoys[choice] + + local escort = ObjEscortGroup:new() + escort:initialize(self, { + maxAmount = 60*7, + amount = 60*7, + proxDist = 400, + target = convoy, + lastUpdate = timer.getAbsTime() + }) + + table.insert(self.objectives, escort) + + local nearzone = "" + local gr = Group.getByName(convoy.name) + if gr and gr:getSize()>0 then + local un = gr:getUnit(1) + local closest = ZoneCommand.getClosestZoneToPoint(un:getPoint()) + if closest then + nearzone = ' near '..closest.name..'' + end + end + + description = description..' Escort convoy'..nearzone..' on route to their destination' + --description = description..'\n Target will be assigned after accepting mission' + + end + + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Escort.lua ]]----------------- + + + +-----------------[[ Missions/TARCAP.lua ]]----------------- + +TARCAP = Mission:new() +do + TARCAP.relevantMissions = { + Mission.types.cas_hard, + Mission.types.dead, + Mission.types.sead, + Mission.types.strike_easy, + Mission.types.strike_hard + } + + function TARCAP:new(id, type, activeMissions) + self = Mission.new(self, id, type) + self:generateObjectivesOverload(activeMissions) + return self + end + + function TARCAP.canCreate(activeMissions) + for _,mis in pairs(activeMissions) do + for _,tp in ipairs(TARCAP.relevantMissions) do + if mis.type == tp then return true end + end + end + end + + function TARCAP:getMissionName() + return 'TARCAP' + end + + function TARCAP:isUnitTypeAllowed(unit) + return unit:hasAttribute('Planes') + end + + function TARCAP:generateObjectivesOverload(activeMissions) + self.completionType = Mission.completion_type.any + local description = '' + local viableMissions = {} + for _,mis in pairs(activeMissions) do + for _,tp in ipairs(TARCAP.relevantMissions) do + if mis.type == tp then + table.insert(viableMissions, mis) + break + end + end + end + + if #viableMissions >= 1 then + local choice = math.random(1,#viableMissions) + local mis = viableMissions[choice] + + local protect = ObjProtectMission:new() + protect:initialize(self, { + mis = mis + }) + + table.insert(self.objectives, protect) + description = description..' Prevent enemy aircraft from interfering with friendly '..mis:getMissionName()..' mission' + if mis.info and mis.info.targetzone then + description = description..' at '..mis.info.targetzone.name + end + + local rewardDef = RewardDefinitions.missions[self.type] + + local kills = ObjAirKillBonus:new() + kills:initialize(self, { + attr = {'Planes'}, + bonus = { + [PlayerTracker.statTypes.xp] = rewardDef.xp.boost + }, + count = 0, + linkedObjectives = {protect} + }) + + table.insert(self.objectives, kills) + + description = description..'\n Aircraft kills increase reward' + end + + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/TARCAP.lua ]]----------------- + + + +-----------------[[ Missions/Recon.lua ]]----------------- + +Recon = Mission:new() +do + function Recon.canCreate() + local zoneNum = 0 + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront == 0 and zone.revealTime == 0 then + return true + end + end + end + + function Recon:getMissionName() + return 'Recon' + end + + function Recon:isUnitTypeAllowed(unit) + return true + end + + function Recon:isInstantReward() + return true + end + + function Recon:generateObjectives() + self.completionType = Mission.completion_type.any + local description = '' + local viableZones = {} + local secondaryViableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront == 0 and zone.revealTime == 0 then + table.insert(viableZones, zone) + end + end + + if #viableZones > 0 then + local choice1 = math.random(1,#viableZones) + local zn1 = viableZones[choice1] + + local recon = ObjReconZone:new() + recon:initialize(self, { + target = zn1, + failZones = { + [1] = {zn1} + } + }) + + table.insert(self.objectives, recon) + description = description..' Observe enemies at '..zn1.name..'\n' + end + + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Recon.lua ]]----------------- + + + +-----------------[[ Missions/BAI.lua ]]----------------- + +BAI = Mission:new() +do + function BAI.canCreate() + return MissionTargetRegistry.baiTargetsAvailable(1) + end + + function BAI:getMissionName() + return 'BAI' + end + + function BAI:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + local viableZones = {} + + local tgt = MissionTargetRegistry.getRandomBaiTarget(1) + + if tgt then + + local gr = Group.getByName(tgt.name) + if gr and gr:getSize()>0 then + local units = {} + for i,v in ipairs(gr:getUnits()) do + units[v:getName()] = false + end + + local kill = ObjDestroyGroup:new() + kill:initialize(self, { + target = tgt, + targetUnitNames = units, + lastUpdate = timer.getAbsTime() + }) + + table.insert(self.objectives, kill) + + local nearzone = "" + local un = gr:getUnit(1) + local closest = ZoneCommand.getClosestZoneToPoint(un:getPoint()) + if closest then + nearzone = ' near '..closest.name..'' + end + + description = description..' Destroy '..tgt.product.display..nearzone..' before it reaches its destination.' + end + + MissionTargetRegistry.removeBaiTarget(tgt) + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/BAI.lua ]]----------------- + + + +-----------------[[ Missions/Anti_Runway.lua ]]----------------- + +Anti_Runway = Mission:new() +do + function Anti_Runway.canCreate() + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront <=2 and zone:hasRunway() then + return true + end + end + end + + function Anti_Runway:getMissionName() + return 'Runway Attack' + end + + function Anti_Runway:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + local viableZones = {} + + local tgts = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.side == 1 and zone.distToFront <=2 and zone:hasRunway() then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(tgts, zone) + end + end + end + + if #tgts > 0 then + local tgt = tgts[math.random(1,#tgts)] + + local rewardDef = RewardDefinitions.missions[self.type] + + local bomb = ObjBombInsideZone:new() + bomb:initialize(self,{ + targetZone = tgt, + max = 20, + required = 5, + dropped = 0, + isFinishStarted = false, + bonus = { + [PlayerTracker.statTypes.xp] = rewardDef.xp.boost + } + }) + + table.insert(self.objectives, bomb) + description = description..' Bomb runway at '..bomb.param.targetZone.name + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Anti_Runway.lua ]]----------------- + + + +-----------------[[ Missions/CSAR.lua ]]----------------- + +CSAR = Mission:new() +do + function CSAR.canCreate() + return MissionTargetRegistry.pilotsAvailableToExtract() + end + + function CSAR:getMissionName() + return 'CSAR' + end + + function CSAR:isInstantReward() + return true + end + + function CSAR:isUnitTypeAllowed(unit) + if PlayerLogistics then + local unitType = unit:getDesc()['typeName'] + return PlayerLogistics.allowedTypes[unitType] and PlayerLogistics.allowedTypes[unitType].personCapacity and PlayerLogistics.allowedTypes[unitType].personCapacity > 0 + end + end + + function CSAR:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + + if MissionTargetRegistry.pilotsAvailableToExtract() then + local tgt = MissionTargetRegistry.getRandomPilot() + + local extract = ObjExtractPilot:new() + extract:initialize(self, { + target = tgt, + loadedBy = nil, + lastUpdate = timer.getAbsTime() + }) + table.insert(self.objectives, extract) + + local unload = ObjUnloadExtractedPilotOrSquad:new() + unload:initialize(self, { + extractObjective = extract + }) + table.insert(self.objectives, unload) + + local nearzone = '' + local closest = ZoneCommand.getClosestZoneToPoint(tgt.pilot:getUnit(1):getPoint()) + if closest then + nearzone = ' near '..closest.name..'' + end + + description = description..' Rescue '..tgt.name..nearzone..' and deliver them to a friendly zone' + --local mgrs = coord.LLtoMGRS(coord.LOtoLL(tgt.pilot:getUnit(1):getPoint())) + --local grid = mist.tostringMGRS(mgrs, 2):gsub(' ','') + --description = description..' ['..grid..']' + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/CSAR.lua ]]----------------- + + + +-----------------[[ Missions/Extraction.lua ]]----------------- + +Extraction = Mission:new() +do + function Extraction.canCreate() + return MissionTargetRegistry.squadsReadyToExtract(2) + end + + function Extraction:getMissionName() + return 'Extraction' + end + + function Extraction:isInstantReward() + return true + end + + function Extraction:isUnitTypeAllowed(unit) + if PlayerLogistics then + local unitType = unit:getDesc()['typeName'] + return PlayerLogistics.allowedTypes[unitType] and PlayerLogistics.allowedTypes[unitType].personCapacity + end + end + + function Extraction:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + + if MissionTargetRegistry.squadsReadyToExtract(2) then + local tgt = MissionTargetRegistry.getRandomSquad(2) + if tgt then + local extract = ObjExtractSquad:new() + extract:initialize(self, { + target = tgt, + loadedBy = nil, + lastUpdate = timer.getAbsTime() + }) + table.insert(self.objectives, extract) + + local unload = ObjUnloadExtractedPilotOrSquad:new() + unload:initialize(self, { + extractObjective = extract + }) + table.insert(self.objectives, unload) + + local infName = PlayerLogistics.getInfantryName(tgt.data.type) + + + local nearzone = '' + local gr = Group.getByName(tgt.name) + if gr and gr:isExist() and gr:getSize()>0 then + local un = gr:getUnit(1) + local closest = ZoneCommand.getClosestZoneToPoint(un:getPoint()) + if closest then + nearzone = ' near '..closest.name..'' + end + --local mgrs = coord.LLtoMGRS(coord.LOtoLL(un:getPoint())) + --local grid = mist.tostringMGRS(mgrs, 2):gsub(' ','') + --description = description..' ['..grid..']' + end + + description = description..' Extract '..infName..nearzone..' to a friendly zone' + end + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/Extraction.lua ]]----------------- + + + +-----------------[[ Missions/DeploySquad.lua ]]----------------- + +DeploySquad = Mission:new() +do + function DeploySquad.canCreate() + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.distToFront and zone.distToFront == 0 then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + return true + end + end + end + end + + function DeploySquad:getMissionName() + return 'Deploy infantry' + end + + function DeploySquad:isInstantReward() + local friendlyDeployments = { + [PlayerLogistics.infantryTypes.engineer] = true, + } + + if self.objectives and self.objectives[1] then + local sqType = self.objectives[1].param.squadType + if friendlyDeployments[sqType] then + return true + end + end + + return false + end + + function DeploySquad:isUnitTypeAllowed(unit) + if PlayerLogistics then + local unitType = unit:getDesc()['typeName'] + return PlayerLogistics.allowedTypes[unitType] and PlayerLogistics.allowedTypes[unitType].personCapacity + end + end + + function DeploySquad:generateObjectives() + self.completionType = Mission.completion_type.all + local description = '' + + local viableZones = {} + for _,zone in pairs(ZoneCommand.getAllZones()) do + if zone.distToFront and zone.distToFront == 0 then + if not MissionTargetRegistry.isZoneTargeted(zone.name) then + table.insert(viableZones, zone) + end + end + end + + if #viableZones > 0 then + local tgt = viableZones[math.random(1,#viableZones)] + if tgt then + local squadType = nil + + if tgt.side == 0 then + squadType = PlayerLogistics.infantryTypes.capture + elseif tgt.side == 1 then + if math.random()>0.5 then + squadType = PlayerLogistics.infantryTypes.sabotage + else + squadType = PlayerLogistics.infantryTypes.spy + end + elseif tgt.side == 2 then + squadType = PlayerLogistics.infantryTypes.engineer + end + + local deploy = ObjDeploySquad:new() + deploy:initialize(self, { + squadType = squadType, + targetZone = tgt, + requiredZoneSide = tgt.side, + unloadedType = nil, + unloadedAt = nil + }) + table.insert(self.objectives, deploy) + + local infName = PlayerLogistics.getInfantryName(squadType) + + description = description..' Deploy '..infName..' to '..tgt.name + + self.info = { + targetzone = tgt + } + end + end + self.description = self.description..description + end +end + +-----------------[[ END OF Missions/DeploySquad.lua ]]----------------- + + + +-----------------[[ RewardDefinitions.lua ]]----------------- + +RewardDefinitions = {} + +do + RewardDefinitions.missions = { + [Mission.types.cap_easy] = { xp = { low = 10, high = 20, boost = 0 } }, + [Mission.types.cap_medium] = { xp = { low = 10, high = 20, boost = 100 } }, + [Mission.types.tarcap] = { xp = { low = 10, high = 10, boost = 150 } }, + [Mission.types.cas_easy] = { xp = { low = 10, high = 20, boost = 0 } }, + [Mission.types.cas_medium] = { xp = { low = 20, high = 30, boost = 0 } }, + [Mission.types.cas_hard] = { xp = { low = 30, high = 40, boost = 0 } }, + [Mission.types.bai] = { xp = { low = 20, high = 30, boost = 0 } }, + [Mission.types.sead] = { xp = { low = 10, high = 20, boost = 0 } }, + [Mission.types.dead] = { xp = { low = 30, high = 40, boost = 0 } }, + [Mission.types.strike_veryeasy] = { xp = { low = 5, high = 10, boost = 0 } }, + [Mission.types.strike_easy] = { xp = { low = 10, high = 20, boost = 0 } }, + [Mission.types.strike_medium] = { xp = { low = 20, high = 30, boost = 0 } }, + [Mission.types.strike_hard] = { xp = { low = 30, high = 40, boost = 0 } }, + [Mission.types.deep_strike] = { xp = { low = 30, high = 40, boost = 0 } }, + [Mission.types.anti_runway] = { xp = { low = 20, high = 30, boost = 25 } }, + [Mission.types.supply_easy] = { xp = { low = 10, high = 20, boost = 0 } }, + [Mission.types.supply_hard] = { xp = { low = 20, high = 30, boost = 0 } }, + [Mission.types.escort] = { xp = { low = 20, high = 30, boost = 0 } }, + [Mission.types.recon] = { xp = { low = 20, high = 30, boost = 0 } }, + [Mission.types.csar] = { xp = { low = 20, high = 30, boost = 0 } }, + [Mission.types.extraction] = { xp = { low = 20, high = 30, boost = 0 } }, + [Mission.types.deploy_squad] = { xp = { low = 20, high = 30, boost = 0 } } + } + + RewardDefinitions.actions = { + pilotExtract = 100, + squadDeploy = 150, + squadExtract = 150, + supplyRatio = 0.06, + supplyBoost = 0.5, + recon = 150 + } +end + +-----------------[[ END OF RewardDefinitions.lua ]]----------------- + + + +-----------------[[ MissionTracker.lua ]]----------------- + +MissionTracker = {} +do + MissionTracker.maxMissionCount = { + [Mission.types.cap_easy] = 2, + [Mission.types.cap_medium] = 1, + [Mission.types.cas_easy] = 2, + [Mission.types.cas_medium] = 1, + [Mission.types.cas_hard] = 1, + [Mission.types.sead] = 3, + [Mission.types.supply_easy] = 3, + [Mission.types.supply_hard] = 1, + [Mission.types.strike_veryeasy] = 2, + [Mission.types.strike_easy] = 1, + [Mission.types.strike_medium] = 3, + [Mission.types.strike_hard] = 1, + [Mission.types.dead] = 1, + [Mission.types.escort] = 2, + [Mission.types.tarcap] = 1, + [Mission.types.deep_strike] = 3, + [Mission.types.recon] = 3, + [Mission.types.bai] = 1, + [Mission.types.anti_runway] = 2, + [Mission.types.csar] = 1, + [Mission.types.extraction] = 1, + [Mission.types.deploy_squad] = 3, + } + + if Config.missions then + for i,v in pairs(Config.missions) do + if MissionTracker.maxMissionCount[i] then + MissionTracker.maxMissionCount[i] = v + end + end + end + + MissionTracker.missionBoardSize = Config.missionBoardSize or 15 + + function MissionTracker:new() + local obj = {} + obj.groupMenus = {} + obj.missionIDPool = {} + obj.missionBoard = {} + obj.activeMissions = {} + + setmetatable(obj, self) + self.__index = self + + DependencyManager.get("MarkerCommands"):addCommand('list', function(event, _, state) + if event.initiator then + state:printMissionBoard(event.initiator:getID(), nil, event.initiator:getGroup():getName()) + elseif world.getPlayer() then + local unit = world.getPlayer() + state:printMissionBoard(unit:getID(), nil, event.initiator:getGroup():getName()) + end + return true + end, nil, obj) + + DependencyManager.get("MarkerCommands"):addCommand('help', function(event, _, state) + if event.initiator then + state:printHelp(event.initiator:getID()) + elseif world.getPlayer() then + local unit = world.getPlayer() + state:printHelp(unit:getID()) + end + return true + end, nil, obj) + + DependencyManager.get("MarkerCommands"):addCommand('active', function(event, _, state) + if event.initiator then + state:printActiveMission(event.initiator:getID(), nil, event.initiator:getPlayerName()) + elseif world.getPlayer() then + state:printActiveMission(nil, nil, world.getPlayer():getPlayerName()) + end + return true + end, nil, obj) + + DependencyManager.get("MarkerCommands"):addCommand('accept',function(event, code, state) + local numcode = tonumber(code) + if not numcode or numcode<1000 or numcode > 9999 then return false end + + local player = '' + local unit = nil + if event.initiator then + player = event.initiator:getPlayerName() + unit = event.initiator + elseif world.getPlayer() then + player = world.getPlayer():getPlayerName() + unit = world.getPlayer() + end + + return state:activateMission(numcode, player, unit) + end, true, obj) + + DependencyManager.get("MarkerCommands"):addCommand('join',function(event, code, state) + local numcode = tonumber(code) + if not numcode or numcode<1000 or numcode > 9999 then return false end + + local player = '' + local unit = nil + if event.initiator then + player = event.initiator:getPlayerName() + unit = event.initiator + elseif world.getPlayer() then + player = world.getPlayer():getPlayerName() + unit = world.getPlayer() + end + + return state:joinMission(numcode, player, unit) + end, true, obj) + + DependencyManager.get("MarkerCommands"):addCommand('leave',function(event, _, state) + local player = '' + if event.initiator then + player = event.initiator:getPlayerName() + elseif world.getPlayer() then + player = world.getPlayer():getPlayerName() + end + + return state:leaveMission(player) + end, nil, obj) + + obj:menuSetup() + obj:start() + + DependencyManager.register("MissionTracker", obj) + return obj + end + + function MissionTracker:menuSetup() + MenuRegistry:register(2, function(event, context) + if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + local groupname = event.initiator:getGroup():getName() + + if context.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid]) + context.groupMenus[groupid] = nil + end + + if not context.groupMenus[groupid] then + local menu = missionCommands.addSubMenuForGroup(groupid, 'Missions') + missionCommands.addCommandForGroup(groupid, 'List Missions', menu, Utils.log(context.printMissionBoard), context, nil, groupid, groupname) + missionCommands.addCommandForGroup(groupid, 'Active Mission', menu, Utils.log(context.printActiveMission), context, nil, groupid, nil, groupname) + + local dial = missionCommands.addSubMenuForGroup(groupid, 'Dial Code', menu) + for i1=1,5,1 do + local digit1 = missionCommands.addSubMenuForGroup(groupid, i1..'___', dial) + for i2=1,5,1 do + local digit2 = missionCommands.addSubMenuForGroup(groupid, i1..i2..'__', digit1) + for i3=1,5,1 do + local digit3 = missionCommands.addSubMenuForGroup(groupid, i1..i2..i3..'_', digit2) + for i4=1,5,1 do + local code = tonumber(i1..i2..i3..i4) + local digit4 = missionCommands.addCommandForGroup(groupid, i1..i2..i3..i4, digit3, Utils.log(context.activateOrJoinMissionForGroup), context, code, groupname) + end + end + end + end + + local leavemenu = missionCommands.addSubMenuForGroup(groupid, 'Leave Mission', menu) + missionCommands.addCommandForGroup(groupid, 'Confirm to leave mission', leavemenu, Utils.log(context.leaveMission), context, player) + missionCommands.addCommandForGroup(groupid, 'Cancel', leavemenu, function() end) + + missionCommands.addCommandForGroup(groupid, 'Help', menu, Utils.log(context.printHelp), context, nil, groupid) + + context.groupMenus[groupid] = menu + end + end + elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + + if context.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid]) + context.groupMenus[groupid] = nil + end + end + end + end, self) + end + + function MissionTracker:printHelp(unitid, groupid) + local msg = 'Missions can only be accepted or joined while landed at a friendly zone.\n' + msg = msg.. 'Rewards from mission completion need to be claimed by landing at a friendly zone.\n\n' + msg = msg.. 'Accept mission:\n' + msg = msg.. ' Each mission has a 4 digit code listed next to its name.\n To accept a mission, either dial its code from the mission radio menu,\n or create a marker on the map and set its text to:\n' + msg = msg.. ' accept:code\n' + msg = msg.. ' (ex. accept:4126)\n\n' + msg = msg.. 'Join mission:\n' + msg = msg.. ' You can team up with other players, by joining a mission they already accepted.\n' + msg = msg.. ' Missions can only be joined if all players who are already part of that mission\n have not taken off yet.\n' + msg = msg.. ' When a mission is completed each player has to land to claim their reward individually.\n' + msg = msg.. ' To join a mission, ask for the join code from a player who is already part of the mission,\n dial it in from the mission radio menu,\n or create a marker on the map and set its text to:\n' + msg = msg.. ' join:code\n' + msg = msg.. ' (ex. join:4126)\n\n' + msg = msg.. 'Map marker commands:\n' + msg = msg.. ' list - displays mission board\n' + msg = msg.. ' accept:code - accepts mission with corresponding code\n' + msg = msg.. ' join:code - joins other players mission with corresponding code\n' + msg = msg.. ' active - displays active mission\n' + msg = msg.. ' leave - leaves active mission\n' + msg = msg.. ' help - displays this message' + + if unitid then + trigger.action.outTextForUnit(unitid, msg, 30) + elseif groupid then + trigger.action.outTextForGroup(groupid, msg, 30) + else + --trigger.action.outText(msg, 30) + end + end + + function MissionTracker:printActiveMission(unitid, groupid, playername, groupname) + if not playername and groupname then + env.info('MissionTracker - printActiveMission: '..tostring(groupname)..' requested group print.') + local gr = Group.getByName(groupname) + for i,v in ipairs(gr:getUnits()) do + if v.getPlayerName and v:getPlayerName() then + self:printActiveMission(v:getID(), gr:getID(), v:getPlayerName()) + end + end + return + end + + local mis = nil + for i,v in pairs(self.activeMissions) do + for pl,un in pairs(v.players) do + if pl == playername then + mis = v + break + end + end + + if mis then break end + end + + local msg = '' + if mis then + msg = mis:getDetailedDescription() + else + msg = 'No active mission' + end + + if unitid then + trigger.action.outTextForUnit(unitid, msg, 30) + elseif groupid then + trigger.action.outTextForGroup(groupid, msg, 30) + else + --trigger.action.outText(msg, 30) + end + end + + function MissionTracker:printMissionBoard(unitid, groupid, groupname) + local gr = Group.getByName(groupname) + local un = gr:getUnit(1) + + local msg = 'Mission Board\n' + local empty = true + local invalidCount = 0 + for i,v in pairs(self.missionBoard) do + if v:isUnitTypeAllowed(un) then + empty = false + msg = msg..'\n'..v:getBriefDescription()..'\n' + else + invalidCount = invalidCount + 1 + end + end + + if empty then + msg = msg..'\n No missions available' + end + + if invalidCount > 0 then + msg = msg..'\n'..invalidCount..' additional missions are not compatible with current aircraft\n' + end + + if unitid then + trigger.action.outTextForUnit(unitid, msg, 30) + elseif groupid then + trigger.action.outTextForGroup(groupid, msg, 30) + else + --trigger.action.outText(msg, 30) + end + end + + function MissionTracker:getNewMissionID() + if #self.missionIDPool == 0 then + for i=1111,5555,1 do + if not tostring(i):find('[06789]') then + if not self.missionBoard[i] and not self.activeMissions[i] then + table.insert(self.missionIDPool, i) + end + end + end + end + + local choice = math.random(1,#self.missionIDPool) + local newId = self.missionIDPool[choice] + table.remove(self.missionIDPool,choice) + return newId + end + + function MissionTracker:start() + timer.scheduleFunction(function(params, time) + for i,v in ipairs(coalition.getPlayers(2)) do + if v and v:isExist() and not Utils.isInAir(v) and v.getPlayerName and v:getPlayerName() then + local player = v:getPlayerName() + local cfg = DependencyManager.get("PlayerTracker"):getPlayerConfig(player) + if cfg.noMissionWarning == true then + local hasMis = false + for _,mis in pairs(params.context.activeMissions) do + if mis.players[player] then + hasMis = true + break + end + end + + if not hasMis then + trigger.action.outTextForUnit(v:getID(), "No mission selected", 9) + end + end + end + end + + return time+10 + end, {context = self}, timer.getTime()+10) + + timer.scheduleFunction(function(param, time) + for code,mis in pairs(param.missionBoard) do + if timer.getAbsTime() - mis.lastStateTime > mis.expireTime then + param.missionBoard[code].state = Mission.states.failed + param.missionBoard[code] = nil + env.info('Mission code'..code..' expired.') + else + mis:updateIsFailed() + if mis.state == Mission.states.failed then + param.missionBoard[code]=nil + env.info('Mission code'..code..' canceled due to objectives failed') + trigger.action.outTextForCoalition(2,'Mission ['..mis.missionID..'] '..mis.name..' was cancelled',5) + end + end + end + + local misCount = Utils.getTableSize(param.missionBoard) + local toGen = MissionTracker.missionBoardSize-misCount + if toGen > 0 then + local validMissions = {} + for _,v in pairs(Mission.types) do + if self:canCreateMission(v) then + table.insert(validMissions,v) + end + end + + if #validMissions > 0 then + for i=1,toGen,1 do + if #validMissions > 0 then + local choice = math.random(1,#validMissions) + local misType = validMissions[choice] + table.remove(validMissions, choice) + param:generateMission(misType) + else + break + end + end + end + end + + return time+1 + end, self, timer.getTime()+1) + + timer.scheduleFunction(function(param, time) + for code,mis in pairs(param.activeMissions) do + -- check if players exist and in same unit as when joined + -- remove from mission if false + for pl,un in pairs(mis.players) do + if not un or + not un:isExist() then + + mis:removePlayer(pl) + env.info('Mission code'..code..' removing player '..pl..', unit no longer exists') + end + end + + -- check if mission has 0 players, delete mission if true + if not mis:hasPlayers() then + param.activeMissions[code]:updateState(Mission.states.failed) + param.activeMissions[code] = nil + env.info('Mission code'..code..' canceled due to no players') + else + --check if mission objectives can still be completed, cancel mission if not + mis:updateIsFailed() + mis:updateIsCompleted() + + if mis.state == Mission.states.preping then + --check if any player in air and move to comencing if true + for pl,un in pairs(mis.players) do + if Utils.isInAir(un) then + mis:updateState(Mission.states.comencing) + mis:pushMessageToPlayers(mis.name..' mission is starting') + break + end + end + elseif mis.state == Mission.states.comencing then + --check if all players in air and move to active if true + --if all players landed, move to preping + local allInAir = true + local allLanded = true + for pl,un in pairs(mis.players) do + if Utils.isInAir(un) then + allLanded = false + else + allInAir = false + end + end + + if allLanded then + mis:updateState(Mission.states.preping) + mis:pushMessageToPlayers(mis.name..' mission is in the prep phase') + end + + if allInAir then + mis:updateState(Mission.states.active) + mis:pushMessageToPlayers(mis.name..' mission has started') + local missionstatus = mis:getDetailedDescription() + mis:pushMessageToPlayers(missionstatus) + end + elseif mis.state == Mission.states.active then + mis:updateObjectives() + elseif mis.state == Mission.states.completed then + local isInstant = mis:isInstantReward() + if isInstant then + mis:pushMessageToPlayers(mis.name..' mission complete.', 60) + else + mis:pushMessageToPlayers(mis.name..' mission complete. Land to claim rewards.', 60) + end + + for _,reward in ipairs(mis.rewards) do + for p,_ in pairs(mis.players) do + local finalAmount = reward.amount + if reward.type == PlayerTracker.statTypes.xp then + finalAmount = math.floor(finalAmount * DependencyManager.get("PlayerTracker"):getPlayerMultiplier(p)) + end + + if isInstant then + DependencyManager.get("PlayerTracker"):addStat(p, finalAmount, reward.type) + mis:pushMessageToPlayer(p, '+'..reward.amount..' '..reward.type) + else + DependencyManager.get("PlayerTracker"):addTempStat(p, finalAmount, reward.type) + end + end + end + + for p,u in pairs(mis.players) do + DependencyManager.get("PlayerTracker"):addRankRewards(p,u, not isInstant) + end + + mis:pushSoundToPlayers("success.ogg") + param.activeMissions[code] = nil + env.info('Mission code'..code..' removed due to completion') + elseif mis.state == Mission.states.failed then + local msg = mis.name..' mission failed.' + if mis.failureReason then + msg = msg..'\n'..mis.failureReason + end + + mis:pushMessageToPlayers(msg, 60) + + mis:pushSoundToPlayers("fail.ogg") + param.activeMissions[code] = nil + env.info('Mission code'..code..' removed due to failure') + end + end + end + + return time+1 + end, self, timer.getTime()+1) + + local ev = {} + ev.context = self + function ev:onEvent(event) + if event.id == world.event.S_EVENT_KILL and event.initiator and event.target and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player and + event.initiator:isExist() and + event.initiator.getCoalition and + event.target.getCoalition and + event.initiator:getCoalition() ~= event.target:getCoalition() then + self.context:tallyKill(player, event.target) + end + end + + if event.id == world.event.S_EVENT_SHOT and event.initiator and event.weapon and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player and event.initiator:isExist() and event.weapon:isExist() then + self.context:tallyWeapon(player, event.weapon) + end + end + end + + world.addEventHandler(ev) + end + + function MissionTracker:generateMission(misType) + local misid = self:getNewMissionID() + env.info('MissionTracker - generating mission type ['..misType..'] id code['..misid..']') + + local newmis = nil + if misType == Mission.types.cap_easy then + newmis = CAP_Easy:new(misid, misType) + elseif misType == Mission.types.cap_medium then + newmis = CAP_Medium:new(misid, misType) + elseif misType == Mission.types.cas_easy then + newmis = CAS_Easy:new(misid, misType) + elseif misType == Mission.types.cas_medium then + newmis = CAS_Medium:new(misid, misType) + elseif misType == Mission.types.cas_hard then + newmis = CAS_Hard:new(misid, misType) + elseif misType == Mission.types.sead then + newmis = SEAD:new(misid, misType) + elseif misType == Mission.types.supply_easy then + newmis = Supply_Easy:new(misid, misType) + elseif misType == Mission.types.supply_hard then + newmis = Supply_Hard:new(misid, misType) + elseif misType == Mission.types.strike_veryeasy then + newmis = Strike_VeryEasy:new(misid, misType) + elseif misType == Mission.types.strike_easy then + newmis = Strike_Easy:new(misid, misType) + elseif misType == Mission.types.strike_medium then + newmis = Strike_Medium:new(misid, misType) + elseif misType == Mission.types.strike_hard then + newmis = Strike_Hard:new(misid, misType) + elseif misType == Mission.types.deep_strike then + newmis = Deep_Strike:new(misid, misType) + elseif misType == Mission.types.dead then + newmis = DEAD:new(misid, misType) + elseif misType == Mission.types.escort then + newmis = Escort:new(misid, misType) + elseif misType == Mission.types.tarcap then + newmis = TARCAP:new(misid, misType, self.activeMissions) + elseif misType == Mission.types.recon then + newmis = Recon:new(misid, misType) + elseif misType == Mission.types.bai then + newmis = BAI:new(misid, misType) + elseif misType == Mission.types.anti_runway then + newmis = Anti_Runway:new(misid, misType) + elseif misType == Mission.types.csar then + newmis = CSAR:new(misid, misType) + elseif misType == Mission.types.extraction then + newmis = Extraction:new(misid, misType) + elseif misType == Mission.types.deploy_squad then + newmis = DeploySquad:new(misid, misType) + end + + if not newmis then return end + + if #newmis.objectives == 0 then return end + + self.missionBoard[misid] = newmis + env.info('MissionTracker - generated mission id code'..misid..' \n'..newmis.description) + trigger.action.outTextForCoalition(2,'New mission available: '..newmis.name,5) + end + + function MissionTracker:tallyWeapon(player, weapon) + for _,m in pairs(self.activeMissions) do + if m.players[player] then + if m.state == Mission.states.active then + if Weapon.getCategoryEx(weapon) == Weapon.Category.BOMB then + timer.scheduleFunction(function (params, time) + if not params.weapon:isExist() then + return nil -- weapon despawned + end + + local alt = Utils.getAGL(params.weapon) + if alt < 5 then + params.mission:tallyWeapon(params.weapon) + return nil + end + + if alt < 20 then + return time+0.01 + end + + return time+0.1 + end, {player = player, weapon = weapon, mission = m}, timer.getTime()+0.1) + end + end + end + end + end + + function MissionTracker:tallyKill(player,kill) + env.info("MissionTracker - tallyKill: "..player.." killed "..kill:getName()) + for _,m in pairs(self.activeMissions) do + if m.players[player] then + if m.state == Mission.states.active then + m:tallyKill(kill) + end + end + end + end + + function MissionTracker:tallySupplies(player, amount, zonename) + env.info("MissionTracker - tallySupplies: "..player.." delivered "..amount.." of supplies to "..zonename) + for _,m in pairs(self.activeMissions) do + if m.players[player] then + if m.state == Mission.states.active then + m:tallySupplies(amount, zonename) + end + end + end + end + + function MissionTracker:tallyLoadPilot(player, pilot) + env.info("MissionTracker - tallyLoadPilot: "..player.." loaded pilot "..pilot.name) + for _,m in pairs(self.activeMissions) do + if m.players[player] then + if m.state == Mission.states.active then + m:tallyLoadPilot(player, pilot) + end + end + end + end + + function MissionTracker:tallyUnloadPilot(player, zonename) + env.info("MissionTracker - tallyUnloadPilot: "..player.." unloaded pilots at "..zonename) + for _,m in pairs(self.activeMissions) do + if m.players[player] then + if m.state == Mission.states.active then + m:tallyUnloadPilot(player, zonename) + end + end + end + end + + function MissionTracker:tallyLoadSquad(player, squad) + env.info("MissionTracker - tallyLoadSquad: "..player.." loaded squad "..squad.name) + for _,m in pairs(self.activeMissions) do + if m.players[player] then + if m.state == Mission.states.active then + m:tallyLoadSquad(player, squad) + end + end + end + end + + function MissionTracker:tallyUnloadSquad(player, zonename, squadType) + env.info("MissionTracker - tallyUnloadSquad: "..player.." unloaded "..squadType.." squad at "..zonename) + for _,m in pairs(self.activeMissions) do + if m.players[player] then + if m.state == Mission.states.active then + m:tallyUnloadSquad(player, zonename, squadType) + end + end + end + end + + function MissionTracker:tallyRecon(player, targetzone, analyzezonename) + env.info("MissionTracker - tallyRecon: "..player.." analyzed "..targetzone.." recon data at "..analyzezonename) + for _,m in pairs(self.activeMissions) do + if m.players[player] then + if m.state == Mission.states.active then + m:tallyRecon(player, targetzone, analyzezonename) + end + end + end + end + + function MissionTracker:activateOrJoinMissionForGroup(code, groupname) + if groupname then + env.info('MissionTracker - activateOrJoinMissionForGroup: '..tostring(groupname)..' requested activate or join '..code) + local gr = Group.getByName(groupname) + for i,v in ipairs(gr:getUnits()) do + if v.getPlayerName and v:getPlayerName() then + local mis = self.activeMissions[code] + if mis then + self:joinMission(code, v:getPlayerName(), v) + else + self:activateMission(code, v:getPlayerName(), v) + end + return + end + end + end + end + + function MissionTracker:activateMission(code, player, unit) + if Config.restrictMissionAcceptance then + if not unit or not unit:isExist() or not Utils.isLanded(unit, true) then + if unit and unit:isExist() then trigger.action.outTextForUnit(unit:getID(), 'Can only accept mission while landed', 5) end + return false + end + + local zn = ZoneCommand.getZoneOfUnit(unit:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(unit:getName()) + end + + if not zn or zn.side ~= unit:getCoalition() then + trigger.action.outTextForUnit(unit:getID(), 'Can only accept mission while inside friendly zone', 5) + return false + end + end + + for c,m in pairs(self.activeMissions) do + if m:getPlayerUnit(player) then + trigger.action.outTextForUnit(unit:getID(), 'A mission is already active.', 5) + return false + end + end + + local mis = self.missionBoard[code] + if not mis then + trigger.action.outTextForUnit(unit:getID(), 'Invalid mission code', 5) + return false + end + + if mis.state ~= Mission.states.new then + trigger.action.outTextForUnit(unit:getID(), 'Invalid mission.', 5) + return false + end + + if not mis:isUnitTypeAllowed(unit) then + trigger.action.outTextForUnit(unit:getID(), 'Current aircraft type is not compatible with this mission.', 5) + return false + end + + self.missionBoard[code] = nil + + trigger.action.outTextForCoalition(2,'Mission ['..mis.missionID..'] '..mis.name..' was accepted by '..player,5) + mis:updateState(Mission.states.preping) + mis.missionID = self:getNewMissionID() + mis:addPlayer(player, unit) + + mis:pushMessageToPlayers(mis.name..' accepted.\nJoin code: ['..mis.missionID..']') + + env.info('Mission code'..code..' changed to code'..mis.missionID) + env.info('Mission code'..mis.missionID..' accepted by '..player) + self.activeMissions[mis.missionID] = mis + return true + end + + function MissionTracker:joinMission(code, player, unit) + if Config.restrictMissionAcceptance then + if not unit or not unit:isExist() or not Utils.isLanded(unit, true) then + if unit and unit:isExist() then trigger.action.outTextForUnit(unit:getID(), 'Can only join mission while landed', 5) end + return false + end + + local zn = ZoneCommand.getZoneOfUnit(unit:getName()) + if not zn then + zn = CarrierCommand.getCarrierOfUnit(unit:getName()) + end + + if not zn or zn.side ~= unit:getCoalition() then + trigger.action.outTextForUnit(unit:getID(), 'Can only join mission while inside friendly zone', 5) + return false + end + end + + for c,m in pairs(self.activeMissions) do + if m:getPlayerUnit(player) then + trigger.action.outTextForUnit(unit:getID(), 'A mission is already active.', 5) + return false + end + end + + local mis = self.activeMissions[code] + if not mis then + trigger.action.outTextForUnit(unit:getID(), 'Invalid mission code', 5) + return false + end + + if mis.state ~= Mission.states.preping then + trigger.action.outTextForUnit(unit:getID(), 'Mission can only be joined if its members have not taken off yet.', 5) + return false + end + + if not mis:isUnitTypeAllowed(unit) then + trigger.action.outTextForUnit(unit:getID(), 'Current aircraft type is not compatible with this mission.', 5) + return false + end + + mis:addPlayer(player, unit) + mis:pushMessageToPlayers(player..' has joined mission '..mis.name) + env.info('Mission code'..code..' joined by '..player) + return true + end + + function MissionTracker:leaveMission(player) + for _,mis in pairs(self.activeMissions) do + if mis:getPlayerUnit(player) then + mis:pushMessageToPlayers(player..' has left mission '..mis.name) + mis:removePlayer(player) + env.info('Mission code'..mis.missionID..' left by '..player) + if not mis:hasPlayers() then + self.activeMissions[mis.missionID]:updateState(Mission.states.failed) + self.activeMissions[mis.missionID] = nil + env.info('Mission code'..mis.missionID..' canceled due to all players leaving') + end + + break + end + end + + return true + end + + function MissionTracker:canCreateMission(misType) + if not MissionTracker.maxMissionCount[misType] then return false end + + local missionCount = 0 + for i,v in pairs(self.missionBoard) do + if v.type == misType then missionCount = missionCount + 1 end + end + + for i,v in pairs(self.activeMissions) do + if v.type == misType then missionCount = missionCount + 1 end + end + + if missionCount >= MissionTracker.maxMissionCount[misType] then return false end + + if misType == Mission.types.cap_easy then + return CAP_Easy.canCreate() + elseif misType == Mission.types.cap_medium then + return CAP_Medium.canCreate() + elseif misType == Mission.types.cas_easy then + return CAS_Easy.canCreate() + elseif misType == Mission.types.cas_medium then + return CAS_Medium.canCreate() + elseif misType == Mission.types.sead then + return SEAD.canCreate() + elseif misType == Mission.types.dead then + return DEAD.canCreate() + elseif misType == Mission.types.cas_hard then + return CAS_Hard.canCreate() + elseif misType == Mission.types.supply_easy then + return Supply_Easy.canCreate() + elseif misType == Mission.types.supply_hard then + return Supply_Hard.canCreate() + elseif misType == Mission.types.strike_veryeasy then + return Strike_VeryEasy.canCreate() + elseif misType == Mission.types.strike_easy then + return Strike_Easy.canCreate() + elseif misType == Mission.types.strike_medium then + return Strike_Medium.canCreate() + elseif misType == Mission.types.strike_hard then + return Strike_Hard.canCreate() + elseif misType == Mission.types.deep_strike then + return Deep_Strike.canCreate() + elseif misType == Mission.types.escort then + return Escort.canCreate() + elseif misType == Mission.types.tarcap then + return TARCAP.canCreate(self.activeMissions) + elseif misType == Mission.types.recon then + return Recon.canCreate() + elseif misType == Mission.types.bai then + return BAI.canCreate() + elseif misType == Mission.types.anti_runway then + return Anti_Runway.canCreate() + elseif misType == Mission.types.csar then + return CSAR.canCreate() + elseif misType == Mission.types.extraction then + return Extraction.canCreate() + elseif misType == Mission.types.deploy_squad then + return DeploySquad.canCreate() + end + + return false + end + +end + + +-----------------[[ END OF MissionTracker.lua ]]----------------- + + + +-----------------[[ SquadTracker.lua ]]----------------- + +SquadTracker = {} +do + function SquadTracker:new() + local obj = {} + obj.activeInfantrySquads = {} + setmetatable(obj, self) + self.__index = self + + obj:start() + + DependencyManager.register("SquadTracker", obj) + return obj + end + + SquadTracker.infantryCallsigns = { + adjectives = {"Sapphire", "Emerald", "Whisper", "Vortex", "Blaze", "Nova", "Silent", "Zephyr", "Radiant", "Shadow", "Lively", "Dynamo", "Dusk", "Rapid", "Stellar", "Tundra", "Obsidian", "Cascade", "Zenith", "Solar"}, + nouns = {"Journey", "Quasar", "Galaxy", "Moonbeam", "Comet", "Starling", "Serenade", "Raven", "Breeze", "Echo", "Avalanche", "Harmony", "Stardust", "Horizon", "Firefly", "Solstice", "Labyrinth", "Whisper", "Cosmos", "Mystique"} + } + + function SquadTracker:generateCallsign() + local adjective = self.infantryCallsigns.adjectives[math.random(1,#self.infantryCallsigns.adjectives)] + local noun = self.infantryCallsigns.nouns[math.random(1,#self.infantryCallsigns.nouns)] + + local callsign = adjective..noun + + if self.activeInfantrySquads[callsign] then + for i=1,1000,1 do + local try = callsign..'-'..i + if not self.activeInfantrySquads[try] then + callsign = try + break + end + end + end + + if not self.activeInfantrySquads[callsign] then + return callsign + end + end + + function SquadTracker:restoreInfantry(save) + + Spawner.createObject(save.name, save.data.name, save.position, save.side, 20, 30,{ + [land.SurfaceType.LAND] = true, + [land.SurfaceType.ROAD] = true, + [land.SurfaceType.RUNWAY] = true, + }) + + self.activeInfantrySquads[save.name] = { + name = save.name, + position = save.position, + state = save.state, + remainingStateTime=save.remainingStateTime, + shouldDiscover = save.discovered, + discovered = save.discovered, + data = save.data + } + + if save.state == "extractReady" then + MissionTargetRegistry.addSquad(self.activeInfantrySquads[save.name]) + end + + env.info('SquadTracker - '..save.name..'('..save.data.type..') restored') + end + + function SquadTracker:spawnInfantry(infantryData, position) + local callsign = self:generateCallsign() + if callsign then + Spawner.createObject(callsign, infantryData.name, position, infantryData.side, 20, 30,{ + [land.SurfaceType.LAND] = true, + [land.SurfaceType.ROAD] = true, + [land.SurfaceType.RUNWAY] = true, + }) + + self:registerInfantry(infantryData, callsign, position) + end + end + + function SquadTracker:registerInfantry(infantryData, groupname, position) + self.activeInfantrySquads[groupname] = {name = groupname, position = position, state = "deployed", remainingStateTime=0, data = infantryData} + + env.info('SquadTracker - '..groupname..'('..infantryData.type..') deployed') + end + + function SquadTracker:start() + if not ZoneCommand then return end + + timer.scheduleFunction(function(param, time) + local self = param.context + + for i,v in pairs(self.activeInfantrySquads) do + local remove = self:processInfantrySquad(v) + if remove then + MissionTargetRegistry.removeSquad(v) + self.activeInfantrySquads[v.name] = nil + end + end + + return time+10 + end, {context = self}, timer.getTime()+1) + end + + function SquadTracker:removeSquad(squadname) + local squad = self.activeInfantrySquads[squadname] + if squad then + MissionTargetRegistry.removeSquad(squad) + squad.state = 'extracted' + squad.remainingStateTime = 0 + self.activeInfantrySquads[squadname] = nil + end + end + + function SquadTracker:getClosestExtractableSquad(sourcePoint, onside) + local minDist = 99999999 + local squad = nil + + for i,v in pairs(self.activeInfantrySquads) do + if v.state == 'extractReady' and v.data.side == onside then + local gr = Group.getByName(v.name) + if gr and gr:getSize()>0 then + local dist = mist.utils.get2DDist(sourcePoint, gr:getUnit(1):getPoint()) + if dist 0 then + for _,tgt in ipairs(targets) do + if tgt.visible and tgt.object then + if tgt.object.isExist and tgt.object:isExist() and tgt.object.getCoalition and tgt.object:getCoalition()~=gr:getCoalition() and + Object.getCategory(tgt.object) == 1 then + local dist = mist.utils.get3DDist(gr:getUnit(1):getPoint(), tgt.object:getPoint()) + if dist < 100 then + isTargetClose = true + break + end + end + end + end + end + + if isTargetClose then + squad.discovered = true + local cnt = gr:getController() + cnt:setCommand({ + id = 'SetInvisible', + params = { + value = false + } + }) + end + elseif squad.shouldDiscover then + squad.shouldDiscover = nil + local cnt = gr:getController() + cnt:setCommand({ + id = 'SetInvisible', + params = { + value = false + } + }) + end + end + end + elseif squad.state == 'extractReady' then + if squad.remainingStateTime <= 0 then + env.info('SquadTracker - '..squad.name..'('..squad.data.type..') extract time elapsed, group MIA') + squad.state = 'mia' + squad.remainingStateTime = 0 + gr:destroy() + MissionTargetRegistry.removeSquad(squad) + return true + end + + if not squad.lastMarkerDeployedTime then + squad.lastMarkerDeployedTime = timer.getAbsTime() - (10*60) + end + + if timer.getAbsTime() - squad.lastMarkerDeployedTime > (5*60) then + if gr:getSize()>0 then + local unPos = gr:getUnit(1):getPoint() + local p = Utils.getPointOnSurface(unPos) + p.x = p.x + math.random(-5,5) + p.z = p.z + math.random(-5,5) + trigger.action.smoke(p, trigger.smokeColor.Blue) + squad.lastMarkerDeployedTime = timer.getAbsTime() + end + end + end + end +end + +-----------------[[ END OF SquadTracker.lua ]]----------------- + + + +-----------------[[ CSARTracker.lua ]]----------------- + +CSARTracker = {} +do + function CSARTracker:new() + local obj = {} + obj.activePilots = {} + setmetatable(obj, self) + self.__index = self + + obj:start() + + DependencyManager.register("CSARTracker", obj) + return obj + end + + function CSARTracker:start() + if not ZoneCommand then return end + + local ev = {} + ev.context = self + function ev:onEvent(event) + if event.id == world.event.S_EVENT_LANDING_AFTER_EJECTION then + if event.initiator and event.initiator:isExist() then + if event.initiator:getCoalition() == 2 then + local z = ZoneCommand.getZoneOfPoint(event.initiator:getPoint()) + if not z then + local name = self.context:generateCallsign() + if name then + local pos = { + x = event.initiator:getPoint().x, + y = event.initiator:getPoint().z + } + + if pos.x ~= 0 and pos.y ~= 0 then + local srfType = land.getSurfaceType(pos) + if srfType ~= land.SurfaceType.WATER and srfType ~= land.SurfaceType.SHALLOW_WATER then + local gr = Spawner.createPilot(name, pos) + self.context:addPilot(name, gr) + end + end + end + end + end + + event.initiator:destroy() + end + end + end + + world.addEventHandler(ev) + + timer.scheduleFunction(function(param, time) + for i,v in pairs(param.context.activePilots) do + v.remainingTime = v.remainingTime - 10 + if not v.pilot:isExist() or v.remainingTime <=0 then + param.context:removePilot(i) + end + end + + return time+10 + end, {context = self}, timer.getTime()+1) + end + + function CSARTracker:markPilot(data) + local pilot = data.pilot + if pilot:isExist() then + local pos = pilot:getUnit(1):getPoint() + local p = Utils.getPointOnSurface(pos) + p.x = p.x + math.random(-5,5) + p.z = p.z + math.random(-5,5) + trigger.action.smoke(p, trigger.smokeColor.Green) + end + end + + function CSARTracker:flarePilot(data) + local pilot = data.pilot + if pilot:isExist() then + local pos = pilot:getUnit(1):getPoint() + local p = Utils.getPointOnSurface(pos) + trigger.action.signalFlare(p, trigger.flareColor.Green, math.random(1,360)) + end + end + + function CSARTracker:removePilot(name) + local data = self.activePilots[name] + if data.pilot and data.pilot:isExist() then data.pilot:destroy() end + + MissionTargetRegistry.removePilot(data) + self.activePilots[name] = nil + end + + function CSARTracker:addPilot(name, pilot) + self.activePilots[name] = {pilot = pilot, name = name, remainingTime = 45*60} + MissionTargetRegistry.addPilot(self.activePilots[name]) + end + + function CSARTracker:restorePilot(save) + local gr = Spawner.createPilot(save.name, save.pos) + + self.activePilots[save.name] = { + pilot = gr, + name = save.name, + remainingTime = save.remainingTime + } + + MissionTargetRegistry.addPilot(self.activePilots[save.name]) + end + + function CSARTracker:getClosestPilot(toPosition) + local minDist = 99999999 + local data = nil + local name = nil + + for i,v in pairs(self.activePilots) do + if v.pilot:isExist() and v.pilot:getSize()>0 and v.pilot:getUnit(1):isExist() and v.remainingTime > 0 then + local dist = mist.utils.get2DDist(toPosition, v.pilot:getUnit(1):getPoint()) + if dist 0 then + local msg = "Warning radius set to "..warningRadius + if metric then + msg=msg.."km" + else + msg=msg.."nm" + end + + local wRadius = 0 + if metric then + wRadius = warningRadius * 1000 + else + wRadius = warningRadius * 1852 + end + + self.players[name] = { + unit = unit, + warningRadius = wRadius, + metric = metric + } + + trigger.action.outTextForUnit(unit:getID(), msg, 10) + DependencyManager.get("PlayerTracker"):setPlayerConfig(name, "gci_warning_radius", warningRadius) + DependencyManager.get("PlayerTracker"):setPlayerConfig(name, "gci_metric", metric) + else + self.players[name] = nil + trigger.action.outTextForUnit(unit:getID(), "GCI Reports disabled", 10) + DependencyManager.get("PlayerTracker"):setPlayerConfig(name, "gci_warning_radius", nil) + DependencyManager.get("PlayerTracker"):setPlayerConfig(name, "gci_metric", nil) + end + end + + function GCI:start() + MenuRegistry:register(4, function(event, context) + if event.id == world.event.S_EVENT_BIRTH and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + local groupname = event.initiator:getGroup():getName() + local unit = event.initiator + + if context.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid]) + context.groupMenus[groupid] = nil + end + + if not context.groupMenus[groupid] then + + local menu = missionCommands.addSubMenuForGroup(groupid, 'GCI') + local setWR = missionCommands.addSubMenuForGroup(groupid, 'Set Warning Radius', menu) + local kmMenu = missionCommands.addSubMenuForGroup(groupid, 'KM', setWR) + local nmMenu = missionCommands.addSubMenuForGroup(groupid, 'NM', setWR) + + missionCommands.addCommandForGroup(groupid, '10 KM', kmMenu, Utils.log(context.registerPlayer), context, player, unit, 10, true) + missionCommands.addCommandForGroup(groupid, '25 KM', kmMenu, Utils.log(context.registerPlayer), context, player, unit, 25, true) + missionCommands.addCommandForGroup(groupid, '50 KM', kmMenu, Utils.log(context.registerPlayer), context, player, unit, 50, true) + missionCommands.addCommandForGroup(groupid, '100 KM', kmMenu, Utils.log(context.registerPlayer), context, player, unit, 100, true) + missionCommands.addCommandForGroup(groupid, '150 KM', kmMenu, Utils.log(context.registerPlayer), context, player, unit, 150, true) + + missionCommands.addCommandForGroup(groupid, '5 NM', nmMenu, Utils.log(context.registerPlayer), context, player, unit, 5, false) + missionCommands.addCommandForGroup(groupid, '10 NM', nmMenu, Utils.log(context.registerPlayer), context, player, unit, 10, false) + missionCommands.addCommandForGroup(groupid, '25 NM', nmMenu, Utils.log(context.registerPlayer), context, player, unit, 25, false) + missionCommands.addCommandForGroup(groupid, '50 NM', nmMenu, Utils.log(context.registerPlayer), context, player, unit, 50, false) + missionCommands.addCommandForGroup(groupid, '80 NM', nmMenu, Utils.log(context.registerPlayer), context, player, unit, 80, false) + missionCommands.addCommandForGroup(groupid, 'Disable Warning Radius', menu, Utils.log(context.registerPlayer), context, player, unit, 0, false) + + context.groupMenus[groupid] = menu + end + end + elseif (event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT or event.id == world.event.S_EVENT_DEAD) and event.initiator and event.initiator.getPlayerName then + local player = event.initiator:getPlayerName() + if player then + local groupid = event.initiator:getGroup():getID() + + if context.groupMenus[groupid] then + missionCommands.removeItemForGroup(groupid, context.groupMenus[groupid]) + context.groupMenus[groupid] = nil + end + end + end + end, self) + + timer.scheduleFunction(function(param, time) + local self = param.context + local allunits = coalition.getGroups(self.side) + + local radars = {} + for _,g in ipairs(allunits) do + for _,u in ipairs(g:getUnits()) do + for _,a in ipairs(self.radarTypes) do + if u:hasAttribute(a) then + table.insert(radars, u) + break + end + end + end + end + + self.radars = radars + env.info("GCI - tracking "..#radars.." radar enabled units") + + return time+10 + end, {context = self}, timer.getTime()+1) + + timer.scheduleFunction(function(param, time) + local self = param.context + + local plyCount = 0 + for i,v in pairs(self.players) do + if not v.unit or not v.unit:isExist() then + self.players[i] = nil + else + plyCount = plyCount + 1 + end + end + + env.info("GCI - reporting to "..plyCount.." players") + if plyCount >0 then + local dect = {} + local dcount = 0 + for _,u in ipairs(self.radars) do + if u:isExist() then + local detected = u:getController():getDetectedTargets(Controller.Detection.RADAR) + for _,d in ipairs(detected) do + if d and d.object and d.object.isExist and d.object:isExist() and + Object.getCategory(d.object) == Object.Category.UNIT and + d.object.getCoalition and + d.object:getCoalition() == self.tgtSide then + + if not dect[d.object:getName()] then + dect[d.object:getName()] = d.object + dcount = dcount + 1 + end + end + end + end + end + + env.info("GCI - aware of "..dcount.." enemy units") + + for name, data in pairs(self.players) do + if data.unit and data.unit:isExist() then + local closeUnits = {} + + local wr = data.warningRadius + if wr > 0 then + for _,dt in pairs(dect) do + if dt:isExist() then + local tgtPnt = dt:getPoint() + local dist = mist.utils.get2DDist(data.unit:getPoint(), tgtPnt) + if dist <= wr then + local brg = math.floor(Utils.getBearing(data.unit:getPoint(), tgtPnt)) + + local myPos = data.unit:getPosition() + local tgtPos = dt:getPosition() + local tgtHeading = math.deg(math.atan2(tgtPos.x.z, tgtPos.x.x)) + local tgtBearing = Utils.getBearing(tgtPos.p, myPos.p) + + local diff = math.abs(Utils.getHeadingDiff(tgtBearing, tgtHeading)) + local aspect = '' + local priority = 1 + if diff <= 30 then + aspect = "Hot" + priority = 1 + elseif diff <= 60 then + aspect = "Flanking" + priority = 1 + elseif diff <= 120 then + aspect = "Beaming" + priority = 2 + else + aspect = "Cold" + priority = 3 + end + + table.insert(closeUnits, { + type = dt:getDesc().typeName, + bearing = brg, + range = dist, + altitude = tgtPnt.y, + score = dist*priority, + aspect = aspect + }) + end + end + end + end + + env.info("GCI - "..#closeUnits.." enemy units within "..wr.."m of "..name) + if #closeUnits > 0 then + table.sort(closeUnits, function(a, b) return a.range < b.range end) + + local msg = "GCI Report:\n" + local count = 0 + for _,tgt in ipairs(closeUnits) do + if data.metric then + local km = tgt.range/1000 + if km < 1 then + msg = msg..'\n'..tgt.type..' MERGED' + else + msg = msg..'\n'..tgt.type..' BRA: '..tgt.bearing..' for ' + msg = msg..Utils.round(km)..'km at ' + msg = msg..(Utils.round(tgt.altitude/250)*250)..'m, ' + msg = msg..tostring(tgt.aspect) + end + else + local nm = tgt.range/1852 + if nm < 1 then + msg = msg..'\n'..tgt.type..' MERGED' + else + msg = msg..'\n'..tgt.type..' BRA: '..tgt.bearing..' for ' + msg = msg..Utils.round(nm)..'nm at ' + msg = msg..(Utils.round((tgt.altitude/0.3048)/1000)*1000)..'ft, ' + msg = msg..tostring(tgt.aspect) + end + end + + count = count + 1 + if count >= 10 then break end + end + + trigger.action.outTextForUnit(data.unit:getID(), msg, 19) + end + else + self.players[name] = nil + end + end + end + + return time+20 + end, {context = self}, timer.getTime()+6) + end +end + +-----------------[[ END OF GCI.lua ]]----------------- + + + +-----------------[[ Starter.lua ]]----------------- + +Starter = {} +do + Starter.neutralChance = 0.1 + + function Starter.start(zones) + if Starter.shouldRandomize() then + Starter.randomize(zones) + else + Starter.normalStart(zones) + end + end + + function Starter.randomize(zones) + local startZones = {} + for _,z in pairs(zones) do + if z.isHeloSpawn and z.isPlaneSpawn then + table.insert(startZones, z) + end + end + + if #startZones > 0 then + local sz = startZones[math.random(1,#startZones)] + + sz:capture(2, true) + Starter.captureNeighbours(sz, math.random(1,3)) + end + + for _,z in pairs(zones) do + if z.side == 0 then + if math.random() > Starter.neutralChance then + z:capture(1,true) + end + end + + if z.side ~= 0 then + z:fullUpgrade(math.random(1,30)/100) + end + end + end + + function Starter.captureNeighbours(zone, stepsLeft) + if stepsLeft > 0 then + for _,v in pairs(zone.neighbours) do + if v.side == 0 then + if math.random() > Starter.neutralChance then + v:capture(2,true) + end + Starter.captureNeighbours(v, stepsLeft-1) + end + end + end + end + + function Starter.shouldRandomize() + if lfs then + local filename = lfs.writedir()..'Missions/Saves/randomize.lua' + if lfs.attributes(filename) then + return true + end + end + end + + function Starter.normalStart(zones) + for _,z in pairs(zones) do + local i = z.initialState + if i then + if i.side and i.side ~= 0 then + z:capture(i.side, true) + z:fullUpgrade() + z:boostProduction(math.random(1,200)) + end + end + end + end +end + +-----------------[[ END OF Starter.lua ]]----------------- + diff --git a/resources/ui/misc/pretense.png b/resources/ui/misc/pretense.png new file mode 100644 index 00000000..0daefbc3 Binary files /dev/null and b/resources/ui/misc/pretense.png differ diff --git a/resources/ui/misc/pretense_discord.png b/resources/ui/misc/pretense_discord.png new file mode 100644 index 00000000..c9d7f544 Binary files /dev/null and b/resources/ui/misc/pretense_discord.png differ diff --git a/resources/ui/misc/pretense_generate.png b/resources/ui/misc/pretense_generate.png new file mode 100644 index 00000000..5fcc00b6 Binary files /dev/null and b/resources/ui/misc/pretense_generate.png differ diff --git a/resources/units/ground_units/LARC-V.yaml b/resources/units/ground_units/LARC-V.yaml index 986ee5d8..b6551bd2 100644 --- a/resources/units/ground_units/LARC-V.yaml +++ b/resources/units/ground_units/LARC-V.yaml @@ -1,4 +1,5 @@ -class: Logistics -price: 2 -variants: - LARC-V: null +class: Logistics +price: 3 +variants: + LARC-V Amphibious Cargo Vehicle: null + LARC-V: null