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