diff --git a/game/db.py b/game/db.py
index c73f7dbf..05eaad06 100644
--- a/game/db.py
+++ b/game/db.py
@@ -1244,7 +1244,7 @@ def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id
-def unit_type_from_name(name: str) -> Optional[UnitType]:
+def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
if name in vehicle_map:
return vehicle_map[name]
elif name in plane_map:
diff --git a/game/event/event.py b/game/event/event.py
index 8cc4aea7..db8e5ee6 100644
--- a/game/event/event.py
+++ b/game/event/event.py
@@ -2,7 +2,7 @@ from __future__ import annotations
import logging
import math
-from typing import Dict, List, Optional, Type, TYPE_CHECKING
+from typing import Dict, List, Optional, TYPE_CHECKING, Type
from dcs.mapping import Point
from dcs.task import Task
@@ -11,12 +11,12 @@ from dcs.unittype import UnitType
from game import db, persistency
from game.debriefing import Debriefing
from game.infos.information import Information
-from game.operation.operation import Operation
+from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance
-from theater import ControlPoint
if TYPE_CHECKING:
from ..game import Game
+ from game.operation.operation import Operation
DIFFICULTY_LOG_BASE = 1.1
EVENT_DEPARTURE_MAX_DISTANCE = 340000
@@ -107,14 +107,16 @@ class Event:
for destroyed_aircraft in debriefing.killed_aircrafts:
try:
cpid = int(destroyed_aircraft.split("|")[3])
- type = db.unit_type_from_name(destroyed_aircraft.split("|")[4])
- if cpid in cp_map.keys():
+ aircraft = db.unit_type_from_name(
+ destroyed_aircraft.split("|")[4])
+ if cpid in cp_map:
cp = cp_map[cpid]
- if type in cp.base.aircraft.keys():
- logging.info("Aircraft destroyed : " + str(type))
- cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1)
- except Exception as e:
- print(e)
+ if aircraft in cp.base.aircraft:
+ logging.info(f"Aircraft destroyed: {aircraft}")
+ cp.base.aircraft[aircraft] = max(
+ 0, cp.base.aircraft[aircraft] - 1)
+ except Exception:
+ logging.exception("Failed to commit destroyed aircraft")
# ------------------------------
# Destroyed ground units
@@ -123,13 +125,13 @@ class Event:
for killed_ground_unit in debriefing.killed_ground_units:
try:
cpid = int(killed_ground_unit.split("|")[3])
- type = db.unit_type_from_name(killed_ground_unit.split("|")[4])
+ aircraft = db.unit_type_from_name(killed_ground_unit.split("|")[4])
if cpid in cp_map.keys():
killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1
cp = cp_map[cpid]
- if type in cp.base.armor.keys():
- logging.info("Ground unit destroyed : " + str(type))
- cp.base.armor[type] = max(0, cp.base.armor[type] - 1)
+ if aircraft in cp.base.armor.keys():
+ logging.info("Ground unit destroyed : " + str(aircraft))
+ cp.base.armor[aircraft] = max(0, cp.base.armor[aircraft] - 1)
except Exception as e:
print(e)
@@ -352,11 +354,13 @@ class Event:
logging.info(info.text)
-
class UnitsDeliveryEvent(Event):
+
informational = True
- def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
+ def __init__(self, attacker_name: str, defender_name: str,
+ from_cp: ControlPoint, to_cp: ControlPoint,
+ game: Game) -> None:
super(UnitsDeliveryEvent, self).__init__(game=game,
location=to_cp.position,
from_cp=from_cp,
@@ -364,17 +368,16 @@ class UnitsDeliveryEvent(Event):
attacker_name=attacker_name,
defender_name=defender_name)
- self.units: Dict[UnitType, int] = {}
+ self.units: Dict[Type[UnitType], int] = {}
- def __str__(self):
+ def __str__(self) -> str:
return "Pending delivery to {}".format(self.to_cp)
- def deliver(self, units: Dict[UnitType, int]):
+ def deliver(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] = self.units.get(k, 0) + v
- def skip(self):
-
+ def skip(self) -> None:
for k, v in self.units.items():
info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn)
self.game.informations.append(info)
diff --git a/game/game.py b/game/game.py
index d69f9cc8..dd03be4f 100644
--- a/game/game.py
+++ b/game/game.py
@@ -26,7 +26,7 @@ from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction
from .infos.information import Information
from .settings import Settings
-from .theater import ConflictTheater, ControlPoint
+from .theater import ConflictTheater, ControlPoint, OffMapSpawn
from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4
@@ -151,7 +151,7 @@ class Game:
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
for cp in self.theater.player_points():
for g in cp.ground_objects:
- if g.category in REWARDS.keys():
+ if g.category in REWARDS.keys() and not g.is_dead:
reward = reward + REWARDS[g.category]
return reward
else:
@@ -160,9 +160,6 @@ class Game:
def _budget_player(self):
self.budget += self.budget_reward_amount
- def awacs_expense_commit(self):
- self.budget -= AWACS_BUDGET_COST
-
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
event = UnitsDeliveryEvent(attacker_name=self.player_name,
defender_name=self.player_name,
@@ -172,10 +169,6 @@ class Game:
self.events.append(event)
return event
- def units_delivery_remove(self, event: Event):
- if event in self.events:
- self.events.remove(event)
-
def initiate_event(self, event: Event):
#assert event in self.events
logging.info("Generating {} (regular)".format(event))
@@ -202,12 +195,6 @@ class Game:
LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater)
- # Save game compatibility.
-
- # TODO: Remove in 2.3.
- if not hasattr(self, "conditions"):
- self.conditions = self.generate_conditions()
-
def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
@@ -248,6 +235,7 @@ class Game:
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
+ cp.pending_unit_deliveries = self.units_delivery_event(cp)
self.aircraft_inventory.set_from_control_point(cp)
# Plan flights & combat for next turn
@@ -274,7 +262,7 @@ class Game:
production = 0.0
for enemy_point in self.theater.enemy_points():
for g in enemy_point.ground_objects:
- if g.category in REWARDS.keys():
+ if g.category in REWARDS.keys() and not g.is_dead:
production = production + REWARDS[g.category]
production = production * 0.75
@@ -289,6 +277,9 @@ class Game:
if len(potential_cp_armor) == 0:
potential_cp_armor = self.theater.enemy_points()
+ potential_cp_armor = [p for p in potential_cp_armor if
+ not isinstance(p, OffMapSpawn)]
+
i = 0
potential_units = db.FACTIONS[self.enemy_name].frontline_units
@@ -325,7 +316,7 @@ class Game:
if i > 50 or budget_for_aircraft <= 0:
break
target_cp = random.choice(potential_cp_armor)
- if target_cp.base.total_planes >= MAX_AIRCRAFT:
+ if target_cp.base.total_aircraft >= MAX_AIRCRAFT:
continue
unit = random.choice(potential_units)
price = db.PRICES[unit] * 2
diff --git a/game/inventory.py b/game/inventory.py
index 89f5afa1..80adb72b 100644
--- a/game/inventory.py
+++ b/game/inventory.py
@@ -1,11 +1,15 @@
"""Inventory management APIs."""
-from collections import defaultdict
-from typing import Dict, Iterable, Iterator, Set, Tuple
+from __future__ import annotations
-from dcs.unittype import UnitType
+from collections import defaultdict
+from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
+
+from dcs.unittype import FlyingType
from gen.flights.flight import Flight
-from theater import ControlPoint
+
+if TYPE_CHECKING:
+ from game.theater import ControlPoint
class ControlPointAircraftInventory:
@@ -13,9 +17,9 @@ class ControlPointAircraftInventory:
def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point
- self.inventory: Dict[UnitType, int] = defaultdict(int)
+ self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
- def add_aircraft(self, aircraft: UnitType, count: int) -> None:
+ def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
"""Adds aircraft to the inventory.
Args:
@@ -24,7 +28,7 @@ class ControlPointAircraftInventory:
"""
self.inventory[aircraft] += count
- def remove_aircraft(self, aircraft: UnitType, count: int) -> None:
+ def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
"""Removes aircraft from the inventory.
Args:
@@ -43,7 +47,7 @@ class ControlPointAircraftInventory:
)
self.inventory[aircraft] -= count
- def available(self, aircraft: UnitType) -> int:
+ def available(self, aircraft: Type[FlyingType]) -> int:
"""Returns the number of available aircraft of the given type.
Args:
@@ -55,14 +59,14 @@ class ControlPointAircraftInventory:
return 0
@property
- def types_available(self) -> Iterator[UnitType]:
+ def types_available(self) -> Iterator[FlyingType]:
"""Iterates over all available aircraft types."""
for aircraft, count in self.inventory.items():
if count > 0:
yield aircraft
@property
- def all_aircraft(self) -> Iterator[Tuple[UnitType, int]]:
+ def all_aircraft(self) -> Iterator[Tuple[FlyingType, int]]:
"""Iterates over all available aircraft types, including amounts."""
for aircraft, count in self.inventory.items():
if count > 0:
@@ -102,9 +106,9 @@ class GlobalAircraftInventory:
return self.inventories[control_point]
@property
- def available_types_for_player(self) -> Iterator[UnitType]:
+ def available_types_for_player(self) -> Iterator[FlyingType]:
"""Iterates over all aircraft types available to the player."""
- seen: Set[UnitType] = set()
+ seen: Set[FlyingType] = set()
for control_point, inventory in self.inventories.items():
if control_point.captured:
for aircraft in inventory.types_available:
diff --git a/game/models/frontline_data.py b/game/models/frontline_data.py
index 94947135..586ebd58 100644
--- a/game/models/frontline_data.py
+++ b/game/models/frontline_data.py
@@ -1,4 +1,4 @@
-from theater import ControlPoint
+from game.theater import ControlPoint
class FrontlineData:
diff --git a/game/operation/operation.py b/game/operation/operation.py
index 0ff06ebe..1e01065b 100644
--- a/game/operation/operation.py
+++ b/game/operation/operation.py
@@ -15,6 +15,7 @@ from dcs.triggers import TriggerStart
from dcs.unittype import UnitType
from game.plugins import LuaPluginManager
+from game.theater import ControlPoint
from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
@@ -29,7 +30,6 @@ from gen.kneeboard import KneeboardGenerator
from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
-from theater import ControlPoint
from .. import db
from ..debriefing import Debriefing
diff --git a/game/settings.py b/game/settings.py
index ad496b65..ff73b63c 100644
--- a/game/settings.py
+++ b/game/settings.py
@@ -6,10 +6,10 @@ from typing import Dict, Optional
class Settings:
# Generator settings
inverted: bool = False
- do_not_generate_carrier: bool = False # TODO : implement
- do_not_generate_lha: bool = False # TODO : implement
- do_not_generate_player_navy: bool = True # TODO : implement
- do_not_generate_enemy_navy: bool = True # TODO : implement
+ do_not_generate_carrier: bool = False
+ do_not_generate_lha: bool = False
+ do_not_generate_player_navy: bool = False
+ do_not_generate_enemy_navy: bool = False
# Difficulty settings
player_skill: str = "Good"
diff --git a/game/theater/base.py b/game/theater/base.py
index 47b3580e..ba8a72f8 100644
--- a/game/theater/base.py
+++ b/game/theater/base.py
@@ -4,9 +4,8 @@ import math
import typing
from typing import Dict, Type
-from dcs.planes import PlaneType
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
-from dcs.unittype import UnitType, VehicleType
+from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.vehicles import AirDefence, Armor
from game import db
@@ -21,20 +20,16 @@ BASE_MIN_STRENGTH = 0
class Base:
- aircraft = {} # type: typing.Dict[PlaneType, int]
- armor = {} # type: typing.Dict[VehicleType, int]
- aa = {} # type: typing.Dict[AirDefence, int]
- strength = 1 # type: float
def __init__(self):
- self.aircraft = {}
- self.armor = {}
- self.aa = {}
+ self.aircraft: Dict[Type[FlyingType], int] = {}
+ self.armor: Dict[VehicleType, int] = {}
+ self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {}
self.strength = 1
@property
- def total_planes(self) -> int:
+ def total_aircraft(self) -> int:
return sum(self.aircraft.values())
@property
@@ -83,7 +78,7 @@ class Base:
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
return result
- def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]:
+ def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]:
return self._find_best_unit(self.aircraft, for_type, count)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
@@ -155,7 +150,7 @@ class Base:
if task:
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
else:
- count = self.total_planes
+ count = self.total_aircraft
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
@@ -167,18 +162,18 @@ class Base:
# previous logic removed because we always want the full air defense capabilities.
return self.total_aa
- def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]:
+ def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def scramble_last_defense(self):
# return as many CAP-capable aircraft as we can since this is the last defense of the base
# (but not more than 20 - that's just nuts)
- return self._find_best_planes(CAP, min(self.total_planes, 20))
+ return self._find_best_planes(CAP, min(self.total_aircraft, 20))
- def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]:
+ def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
- def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]:
+ def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def assemble_attack(self) -> typing.Dict[Armor, int]:
diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py
index c0b373ce..7766c90e 100644
--- a/game/theater/conflicttheater.py
+++ b/game/theater/conflicttheater.py
@@ -1,13 +1,28 @@
from __future__ import annotations
-import logging
+import itertools
import json
+import logging
from dataclasses import dataclass
+from functools import cached_property
from itertools import tee
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
+from dcs import Mission
+from dcs.countries import (
+ CombinedJointTaskForcesBlue,
+ CombinedJointTaskForcesRed,
+)
+from dcs.country import Country
from dcs.mapping import Point
+from dcs.planes import F_15C
+from dcs.ships import (
+ CVN_74_John_C__Stennis,
+ LHA_1_Tarawa,
+ USS_Arleigh_Burke_IIa,
+)
+from dcs.statics import Fortification
from dcs.terrain import (
caucasus,
nevada,
@@ -16,11 +31,20 @@ from dcs.terrain import (
syria,
thechannel,
)
-from dcs.terrain.terrain import Terrain
+from dcs.terrain.terrain import Airport, Terrain
+from dcs.unitgroup import (
+ FlyingGroup,
+ Group,
+ ShipGroup,
+ StaticGroup,
+ VehicleGroup,
+)
+from dcs.vehicles import AirDefence, Armor
from gen.flights.flight import FlightType
-from .controlpoint import ControlPoint, MissionTarget
+from .controlpoint import ControlPoint, MissionTarget, OffMapSpawn
from .landmap import Landmap, load_landmap, poly_contains
+from ..utils import nm_to_meter
Numeric = Union[int, float]
@@ -73,6 +97,266 @@ def pairwise(iterable):
return zip(a, b)
+class MizCampaignLoader:
+ BLUE_COUNTRY = CombinedJointTaskForcesBlue()
+ RED_COUNTRY = CombinedJointTaskForcesRed()
+
+ OFF_MAP_UNIT_TYPE = F_15C.id
+
+ CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
+ LHA_UNIT_TYPE = LHA_1_Tarawa.id
+ FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
+
+ EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
+ SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id
+ GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id
+ STRIKE_TARGET_UNIT_TYPE = Fortification.Workshop_A.id
+ OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
+ SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
+
+ # Multiple options for the required SAMs so campaign designers can more
+ # easily see the coverage of their IADS. Designers focused on campaigns that
+ # will primarily use SA-2s can place SA-2 launchers to ensure that they will
+ # have adequate coverage, and designers focused on campaigns that will
+ # primarily use SA-10s can do the same.
+ REQUIRED_SAM_UNIT_TYPES = {
+ AirDefence.SAM_Hawk_LN_M192,
+ AirDefence.SAM_Patriot_LN_M901,
+ AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
+ AirDefence.SAM_SA_10_S_300PS_LN_5P85D,
+ AirDefence.SAM_SA_2_LN_SM_90,
+ AirDefence.SAM_SA_3_S_125_LN_5P73,
+ }
+
+ BASE_DEFENSE_RADIUS = nm_to_meter(2)
+
+ def __init__(self, miz: Path, theater: ConflictTheater) -> None:
+ self.theater = theater
+ self.mission = Mission()
+ self.mission.load_file(str(miz))
+ self.control_point_id = itertools.count(1000)
+
+ # If there are no red carriers there usually aren't red units. Make sure
+ # both countries are initialized so we don't have to deal with None.
+ if self.mission.country(self.BLUE_COUNTRY.name) is None:
+ self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY)
+ if self.mission.country(self.RED_COUNTRY.name) is None:
+ self.mission.coalition["red"].add_country(self.RED_COUNTRY)
+
+ @staticmethod
+ def control_point_from_airport(airport: Airport) -> ControlPoint:
+ # TODO: Radials?
+ radials = LAND
+
+ # The wiki says this is a legacy property and to just use regular.
+ size = SIZE_REGULAR
+
+ # The importance is taken from the periodicity of the airport's
+ # warehouse divided by 10. 30 is the default, and out of range (valid
+ # values are between 1.0 and 1.4). If it is used, pick the default
+ # importance.
+ if airport.periodicity == 30:
+ importance = IMPORTANCE_MEDIUM
+ else:
+ importance = airport.periodicity / 10
+
+ cp = ControlPoint.from_airport(airport, radials, size, importance)
+ cp.captured = airport.is_blue()
+
+ # Use the unlimited aircraft option to determine if an airfield should
+ # be owned by the player when the campaign is "inverted".
+ cp.captured_invert = airport.unlimited_aircrafts
+
+ return cp
+
+ def country(self, blue: bool) -> Country:
+ country = self.mission.country(
+ self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name)
+ # Should be guaranteed because we initialized them.
+ assert country
+ return country
+
+ @property
+ def blue(self) -> Country:
+ return self.country(blue=True)
+
+ @property
+ def red(self) -> Country:
+ return self.country(blue=False)
+
+ def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
+ for group in self.country(blue).plane_group:
+ if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
+ yield group
+
+ def carriers(self, blue: bool) -> Iterator[ShipGroup]:
+ for group in self.country(blue).ship_group:
+ if group.units[0].type == self.CV_UNIT_TYPE:
+ yield group
+
+ def lhas(self, blue: bool) -> Iterator[ShipGroup]:
+ for group in self.country(blue).ship_group:
+ if group.units[0].type == self.LHA_UNIT_TYPE:
+ yield group
+
+ @property
+ def ships(self) -> Iterator[ShipGroup]:
+ for group in self.blue.ship_group:
+ if group.units[0].type == self.SHIP_UNIT_TYPE:
+ yield group
+
+ @property
+ def ewrs(self) -> Iterator[VehicleGroup]:
+ for group in self.blue.vehicle_group:
+ if group.units[0].type == self.EWR_UNIT_TYPE:
+ yield group
+
+ @property
+ def sams(self) -> Iterator[VehicleGroup]:
+ for group in self.blue.vehicle_group:
+ if group.units[0].type == self.SAM_UNIT_TYPE:
+ yield group
+
+ @property
+ def garrisons(self) -> Iterator[VehicleGroup]:
+ for group in self.blue.vehicle_group:
+ if group.units[0].type == self.GARRISON_UNIT_TYPE:
+ yield group
+
+ @property
+ def strike_targets(self) -> Iterator[StaticGroup]:
+ for group in self.blue.static_group:
+ if group.units[0].type == self.STRIKE_TARGET_UNIT_TYPE:
+ yield group
+
+ @property
+ def offshore_strike_targets(self) -> Iterator[StaticGroup]:
+ for group in self.blue.static_group:
+ if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
+ yield group
+
+ @property
+ def required_sams(self) -> Iterator[VehicleGroup]:
+ for group in self.red.vehicle_group:
+ if group.units[0].type == self.REQUIRED_SAM_UNIT_TYPES:
+ yield group
+
+ @cached_property
+ def control_points(self) -> Dict[int, ControlPoint]:
+ control_points = {}
+ for airport in self.mission.terrain.airport_list():
+ if airport.is_blue() or airport.is_red():
+ control_point = self.control_point_from_airport(airport)
+ control_points[control_point.id] = control_point
+
+ for blue in (False, True):
+ for group in self.off_map_spawns(blue):
+ control_point = OffMapSpawn(next(self.control_point_id),
+ str(group.name), group.position)
+ control_point.captured = blue
+ control_point.captured_invert = group.late_activation
+ control_points[control_point.id] = control_point
+ for group in self.carriers(blue):
+ # TODO: Name the carrier.
+ control_point = ControlPoint.carrier(
+ "carrier", group.position, next(self.control_point_id))
+ control_point.captured = blue
+ control_point.captured_invert = group.late_activation
+ control_points[control_point.id] = control_point
+ for group in self.lhas(blue):
+ # TODO: Name the LHA.
+ control_point = ControlPoint.lha(
+ "lha", group.position, next(self.control_point_id))
+ control_point.captured = blue
+ control_point.captured_invert = group.late_activation
+ control_points[control_point.id] = control_point
+
+ return control_points
+
+ @property
+ def front_line_path_groups(self) -> Iterator[VehicleGroup]:
+ for group in self.country(blue=True).vehicle_group:
+ if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
+ yield group
+
+ @cached_property
+ def front_lines(self) -> Dict[str, ComplexFrontLine]:
+ # Dict of front line ID to a front line.
+ front_lines = {}
+ for group in self.front_line_path_groups:
+ # The unit will have its first waypoint at the source CP and the
+ # final waypoint at the destination CP. Intermediate waypoints
+ # define the curve of the front line.
+ waypoints = [p.position for p in group.points]
+ origin = self.mission.terrain.nearest_airport(waypoints[0])
+ if origin is None:
+ raise RuntimeError(
+ f"No airport near the first waypoint of {group.name}")
+ destination = self.mission.terrain.nearest_airport(waypoints[-1])
+ if destination is None:
+ raise RuntimeError(
+ f"No airport near the final waypoint of {group.name}")
+
+ # Snap the begin and end points to the control points.
+ waypoints[0] = origin.position
+ waypoints[-1] = destination.position
+ front_line_id = f"{origin.id}|{destination.id}"
+ front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
+ self.control_points[origin.id].connect(
+ self.control_points[destination.id])
+ self.control_points[destination.id].connect(
+ self.control_points[origin.id])
+ return front_lines
+
+ def objective_info(self, group: Group) -> Tuple[ControlPoint, int]:
+ closest = self.theater.closest_control_point(group.position)
+ distance = closest.position.distance_to_point(group.position)
+ return closest, distance
+
+ def add_preset_locations(self) -> None:
+ for group in self.garrisons:
+ closest, distance = self.objective_info(group)
+ if distance < self.BASE_DEFENSE_RADIUS:
+ closest.preset_locations.base_garrisons.append(group.position)
+ else:
+ logging.warning(
+ f"Found garrison unit too far from base: {group.name}")
+
+ for group in self.sams:
+ closest, distance = self.objective_info(group)
+ if distance < self.BASE_DEFENSE_RADIUS:
+ closest.preset_locations.base_air_defense.append(group.position)
+ else:
+ closest.preset_locations.sams.append(group.position)
+
+ for group in self.ewrs:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.ewrs.append(group.position)
+
+ for group in self.strike_targets:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.strike_locations.append(group.position)
+
+ for group in self.offshore_strike_targets:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.offshore_strike_locations.append(
+ group.position)
+
+ for group in self.ships:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.ships.append(group.position)
+
+ for group in self.required_sams:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.required_sams.append(group.position)
+
+ def populate_theater(self) -> None:
+ for control_point in self.control_points.values():
+ self.theater.add_controlpoint(control_point)
+ self.add_preset_locations()
+ self.theater.set_frontline_data(self.front_lines)
+
+
class ConflictTheater:
terrain: Terrain
@@ -83,17 +367,35 @@ class ConflictTheater:
land_poly = None # type: Polygon
"""
daytime_map: Dict[str, Tuple[int, int]]
- frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
+ _frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
def __init__(self):
self.controlpoints: List[ControlPoint] = []
- self.frontline_data = FrontLine.load_json_frontlines(self)
+ self._frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
"""
self.land_poly = geometry.Polygon(self.landmap[0][0])
for x in self.landmap[1]:
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
"""
+ @property
+ def frontline_data(self) -> Optional[Dict[str, ComplexFrontLine]]:
+ if self._frontline_data is None:
+ self.load_frontline_data_from_file()
+ return self._frontline_data
+
+ def load_frontline_data_from_file(self) -> None:
+ if self._frontline_data is not None:
+ logging.warning("Replacing existing frontline data from file")
+ self._frontline_data = FrontLine.load_json_frontlines(self)
+ if self._frontline_data is None:
+ self._frontline_data = {}
+
+ def set_frontline_data(self, data: Dict[str, ComplexFrontLine]) -> None:
+ if self._frontline_data is not None:
+ logging.warning("Replacing existing frontline data")
+ self._frontline_data = data
+
def add_controlpoint(self, point: ControlPoint,
connected_to: Optional[List[ControlPoint]] = None):
if connected_to is None:
@@ -153,11 +455,21 @@ class ConflictTheater:
def enemy_points(self) -> List[ControlPoint]:
return [point for point in self.controlpoints if not point.captured]
+ def closest_control_point(self, point: Point) -> ControlPoint:
+ closest = self.controlpoints[0]
+ closest_distance = point.distance_to_point(closest.position)
+ for control_point in self.controlpoints[1:]:
+ distance = point.distance_to_point(control_point.position)
+ if distance < closest_distance:
+ closest = control_point
+ closest_distance = distance
+ return closest
+
def add_json_cp(self, theater, p: dict) -> ControlPoint:
if p["type"] == "airbase":
- airbase = theater.terrain.airports[p["id"]].__class__
+ airbase = theater.terrain.airports[p["id"]]
if "radials" in p.keys():
radials = p["radials"]
@@ -188,7 +500,7 @@ class ConflictTheater:
return cp
@staticmethod
- def from_json(data: Dict[str, Any]) -> ConflictTheater:
+ def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
theaters = {
"Caucasus": CaucasusTheater,
"Nevada": NevadaTheater,
@@ -199,6 +511,12 @@ class ConflictTheater:
}
theater = theaters[data["theater"]]
t = theater()
+
+ miz = data.get("miz", None)
+ if miz is not None:
+ MizCampaignLoader(directory / miz, t).populate_theater()
+ return t
+
cps = {}
for p in data["player_points"]:
cp = t.add_json_cp(theater, p)
@@ -376,10 +694,6 @@ class FrontLine(MissionTarget):
"""Returns a tuple of the two control points."""
return self.control_point_a, self.control_point_b
- @property
- def middle_point(self):
- self.point_from_a(self.attack_distance / 2)
-
@property
def attack_distance(self):
"""The total distance of all segments"""
diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py
index 46ac7e00..476f831a 100644
--- a/game/theater/controlpoint.py
+++ b/game/theater/controlpoint.py
@@ -1,9 +1,12 @@
from __future__ import annotations
import itertools
+import logging
+import random
import re
+from dataclasses import dataclass, field
from enum import Enum
-from typing import Dict, Iterator, List, TYPE_CHECKING
+from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
from dcs.mapping import Point
from dcs.ships import (
@@ -13,6 +16,7 @@ from dcs.ships import (
Type_071_Amphibious_Transport_Dock,
)
from dcs.terrain.terrain import Airport
+from dcs.unittype import FlyingType
from game import db
from gen.ground_forces.combat_stance import CombatStance
@@ -20,12 +24,16 @@ from .base import Base
from .missiontarget import MissionTarget
from .theatergroundobject import (
BaseDefenseGroundObject,
+ EwrGroundObject,
+ SamGroundObject,
TheaterGroundObject,
+ VehicleGroupGroundObject,
)
if TYPE_CHECKING:
from game import Game
from gen.flights.flight import FlightType
+ from ..event import UnitsDeliveryEvent
class ControlPointType(Enum):
@@ -34,6 +42,87 @@ class ControlPointType(Enum):
LHA_GROUP = 2 # A group with a Tarawa carrier (Helicopters & Harrier)
FARP = 4 # A FARP, with slots for helicopters
FOB = 5 # A FOB (ground units only)
+ OFF_MAP = 6
+
+
+class LocationType(Enum):
+ BaseAirDefense = "base air defense"
+ Coastal = "coastal defense"
+ Ewr = "EWR"
+ Garrison = "garrison"
+ MissileSite = "missile site"
+ OffshoreStrikeTarget = "offshore strike target"
+ Sam = "SAM"
+ Ship = "ship"
+ Shorad = "SHORAD"
+ StrikeTarget = "strike target"
+
+
+@dataclass
+class PresetLocations:
+ """Defines the preset locations loaded from the campaign mission file."""
+
+ #: Locations used for spawning ground defenses for bases.
+ base_garrisons: List[Point] = field(default_factory=list)
+
+ #: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
+ #: and SHORADs.
+ base_air_defense: List[Point] = field(default_factory=list)
+
+ #: Locations used by EWRs.
+ ewrs: List[Point] = field(default_factory=list)
+
+ #: Locations used by SAMs outside of bases.
+ sams: List[Point] = field(default_factory=list)
+
+ #: Locations used by non-carrier ships. Carriers and LHAs are not random.
+ ships: List[Point] = field(default_factory=list)
+
+ #: Locations used by coastal defenses.
+ coastal_defenses: List[Point] = field(default_factory=list)
+
+ #: Locations used by ground based strike objectives.
+ strike_locations: List[Point] = field(default_factory=list)
+
+ #: Locations used by offshore strike objectives.
+ offshore_strike_locations: List[Point] = field(default_factory=list)
+
+ #: Locations of SAMs which should always be spawned.
+ required_sams: List[Point] = field(default_factory=list)
+
+ @staticmethod
+ def _random_from(points: List[Point]) -> Optional[Point]:
+ """Finds, removes, and returns a random position from the given list."""
+ if not points:
+ return None
+ point = random.choice(points)
+ points.remove(point)
+ return point
+
+ def random_for(self, location_type: LocationType) -> Optional[Point]:
+ """Returns a position suitable for the given location type.
+
+ The location, if found, will be claimed by the caller and not available
+ to subsequent calls.
+ """
+ if location_type == LocationType.Garrison:
+ return self._random_from(self.base_garrisons)
+ if location_type == LocationType.Sam:
+ return self._random_from(self.sams)
+ if location_type == LocationType.BaseAirDefense:
+ return self._random_from(self.base_air_defense)
+ if location_type == LocationType.Ewr:
+ return self._random_from(self.ewrs)
+ if location_type == LocationType.Shorad:
+ return self._random_from(self.base_garrisons)
+ if location_type == LocationType.OffshoreStrikeTarget:
+ return self._random_from(self.offshore_strike_locations)
+ if location_type == LocationType.Ship:
+ return self._random_from(self.ships)
+ if location_type == LocationType.StrikeTarget:
+ return self._random_from(self.strike_locations)
+ logging.error(f"Unknown location type: {location_type}")
+ return None
class ControlPoint(MissionTarget):
@@ -57,6 +146,7 @@ class ControlPoint(MissionTarget):
self.at = at
self.connected_objectives: List[TheaterGroundObject] = []
self.base_defenses: List[BaseDefenseGroundObject] = []
+ self.preset_locations = PresetLocations()
self.size = size
self.importance = importance
@@ -69,6 +159,7 @@ class ControlPoint(MissionTarget):
self.cptype = cptype
self.stances: Dict[int, CombatStance] = {}
self.airport = None
+ self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None
@property
def ground_objects(self) -> List[TheaterGroundObject]:
@@ -79,7 +170,7 @@ class ControlPoint(MissionTarget):
def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True):
assert airport
obj = cls(airport.id, airport.name, airport.position, airport, radials, size, importance, has_frontline, cptype=ControlPointType.AIRBASE)
- obj.airport = airport()
+ obj.airport = airport
return obj
@classmethod
@@ -144,7 +235,7 @@ class ControlPoint(MissionTarget):
return result
@property
- def available_aircraft_slots(self):
+ def total_aircraft_parking(self):
"""
:return: The maximum number of aircraft that can be stored in this control point
"""
@@ -157,7 +248,7 @@ class ControlPoint(MissionTarget):
else:
return 0
- def connect(self, to):
+ def connect(self, to: ControlPoint) -> None:
self.connected_points.append(to)
self.stances[to.id] = CombatStance.DEFENSIVE
@@ -222,6 +313,24 @@ class ControlPoint(MissionTarget):
def is_friendly(self, to_player: bool) -> bool:
return self.captured == to_player
+ def clear_base_defenses(self) -> None:
+ for base_defense in self.base_defenses:
+ if isinstance(base_defense, EwrGroundObject):
+ self.preset_locations.ewrs.append(base_defense.position)
+ elif isinstance(base_defense, SamGroundObject):
+ self.preset_locations.base_air_defense.append(
+ base_defense.position)
+ elif isinstance(base_defense, VehicleGroupGroundObject):
+ self.preset_locations.base_garrisons.append(
+ base_defense.position)
+ else:
+ logging.error(
+ "Could not determine preset location type for "
+ f"{base_defense}. Assuming garrison type.")
+ self.preset_locations.base_garrisons.append(
+ base_defense.position)
+ self.base_defenses = []
+
def capture(self, game: Game, for_player: bool) -> None:
if for_player:
self.captured = True
@@ -233,9 +342,8 @@ class ControlPoint(MissionTarget):
self.base.aircraft = {}
self.base.armor = {}
- # Handle cyclic dependency.
+ self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator
- self.base_defenses = []
BaseDefenseGenerator(game, self).generate()
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
@@ -260,3 +368,41 @@ class ControlPoint(MissionTarget):
yield from [
# TODO: FlightType.STRIKE
]
+
+ def can_land(self, aircraft: FlyingType) -> bool:
+ if self.is_carrier and aircraft not in db.CARRIER_CAPABLE:
+ return False
+ if self.is_lha and aircraft not in db.LHA_CAPABLE:
+ return False
+ return True
+
+ @property
+ def expected_aircraft_next_turn(self) -> int:
+ total = self.base.total_aircraft
+ assert self.pending_unit_deliveries
+ for unit_bought in self.pending_unit_deliveries.units:
+ if issubclass(unit_bought, FlyingType):
+ total += self.pending_unit_deliveries.units[unit_bought]
+ return total
+
+ @property
+ def unclaimed_parking(self) -> int:
+ return self.total_aircraft_parking - self.expected_aircraft_next_turn
+
+
+class OffMapSpawn(ControlPoint):
+ def __init__(self, id: int, name: str, position: Point):
+ from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
+ super().__init__(id, name, position, at=position, radials=[],
+ size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM,
+ has_frontline=False, cptype=ControlPointType.OFF_MAP)
+
+ def capture(self, game: Game, for_player: bool) -> None:
+ raise RuntimeError("Off map control points cannot be captured")
+
+ def mission_types(self, for_player: bool) -> Iterator[FlightType]:
+ yield from []
+
+ @property
+ def total_aircraft_parking(self) -> int:
+ return 1000
diff --git a/game/theater/frontline.py b/game/theater/frontline.py
deleted file mode 100644
index 3b57f9b6..00000000
--- a/game/theater/frontline.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Only here to keep compatibility for save games generated in version 2.2.0"""
diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py
index 95bc1c69..c5232a32 100644
--- a/game/theater/start_generator.py
+++ b/game/theater/start_generator.py
@@ -4,7 +4,7 @@ import logging
import math
import pickle
import random
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, Optional
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
@@ -13,6 +13,18 @@ from dcs.vehicles import AirDefence
from game import Game, db
from game.factions.faction import Faction
from game.settings import Settings
+from game.theater import LocationType
+from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
+from game.theater.theatergroundobject import (
+ BuildingGroundObject,
+ CarrierGroundObject,
+ EwrGroundObject,
+ LhaGroundObject,
+ MissileSiteGroundObject,
+ SamGroundObject,
+ ShipGroundObject,
+ VehicleGroupGroundObject,
+)
from game.version import VERSION
from gen import namegen
from gen.defenses.armor_group_generator import generate_armor_group
@@ -21,29 +33,17 @@ from gen.fleet.ship_group_generator import (
generate_lha_group,
generate_ship_group,
)
-from gen.locations.preset_location_finder import PresetLocationFinder
-from gen.locations.preset_locations import PresetLocation
+from gen.locations.preset_location_finder import MizDataLocationFinder
from gen.missiles.missiles_group_generator import generate_missile_group
from gen.sam.sam_group_generator import (
generate_anti_air_group,
generate_ewr_group, generate_shorad_group,
)
-from theater import (
+from . import (
ConflictTheater,
ControlPoint,
ControlPointType,
- TheaterGroundObject,
-)
-from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
-from game.theater.theatergroundobject import (
- EwrGroundObject,
- SamGroundObject,
- BuildingGroundObject,
- CarrierGroundObject,
- LhaGroundObject,
- MissileSiteGroundObject,
- ShipGroundObject,
- VehicleGroupGroundObject,
+ OffMapSpawn,
)
GroundObjectTemplates = Dict[str, Dict[str, Any]]
@@ -139,7 +139,13 @@ class GameGenerator:
control_point.base.commision_points = {}
control_point.base.strength = 1
+ # The tasks here are confusing. PinpointStrike for some reason means
+ # ground units.
for task in [PinpointStrike, CAP, CAS, AirDefence]:
+ if isinstance(control_point, OffMapSpawn):
+ # Off-map spawn locations start with no aircraft.
+ continue
+
if IMPORTANCE_HIGH <= control_point.importance <= IMPORTANCE_LOW:
raise ValueError(
f"CP importance must be between {IMPORTANCE_LOW} and "
@@ -164,11 +170,155 @@ class GameGenerator:
control_point.base.commision_units({unit_type: count_per_type})
+class LocationFinder:
+ def __init__(self, game: Game, control_point: ControlPoint) -> None:
+ self.game = game
+ self.control_point = control_point
+ self.miz_data = MizDataLocationFinder.compute_possible_locations(
+ game.theater.terrain.name, control_point.full_name)
+
+ def location_for(self, location_type: LocationType) -> Optional[Point]:
+ position = self.control_point.preset_locations.random_for(location_type)
+ if position is not None:
+ return position
+
+ logging.warning(f"No campaign location for %s at %s",
+ location_type.value, self.control_point)
+ position = self.random_from_miz_data(
+ location_type == LocationType.OffshoreStrikeTarget)
+ if position is not None:
+ return position
+
+ logging.debug(f"No mizdata location for %s at %s", location_type.value,
+ self.control_point)
+ position = self.random_position(location_type)
+ if position is not None:
+ return position
+
+ logging.error(f"Could not find position for %s at %s",
+ location_type.value, self.control_point)
+ return None
+
+ def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
+ if offshore:
+ locations = self.miz_data.offshore_locations
+ else:
+ locations = self.miz_data.ashore_locations
+ if self.miz_data.offshore_locations:
+ preset = random.choice(locations)
+ locations.remove(preset)
+ return preset.position
+ return None
+
+ def random_position(self, location_type: LocationType) -> Optional[Point]:
+ # TODO: Flesh out preset locations so we never hit this case.
+ logging.warning("Falling back to random location for %s at %s",
+ location_type.value, self.control_point)
+
+ is_base_defense = location_type in {
+ LocationType.BaseAirDefense,
+ LocationType.Garrison,
+ LocationType.Shorad,
+ }
+
+ on_land = location_type not in {
+ LocationType.OffshoreStrikeTarget,
+ LocationType.Ship,
+ }
+
+ avoid_others = location_type not in {
+ LocationType.Garrison,
+ LocationType.MissileSite,
+ LocationType.Sam,
+ LocationType.Ship,
+ LocationType.Shorad,
+ }
+
+ if is_base_defense:
+ min_range = 400
+ max_range = 3200
+ elif location_type == LocationType.Ship:
+ min_range = 5000
+ max_range = 40000
+ elif location_type == LocationType.MissileSite:
+ min_range = 2500
+ max_range = 40000
+ else:
+ min_range = 10000
+ max_range = 40000
+
+ position = self._find_random_position(min_range, max_range,
+ on_land, is_base_defense,
+ avoid_others)
+
+ # Retry once, searching a bit further (On some big airbases, 3200 is too
+ # short (Ex : Incirlik)), but searching farther on every base would be
+ # problematic, as some base defense units would end up very far away
+ # from small airfields.
+ if position is None and is_base_defense:
+ position = self._find_random_position(3200, 4800,
+ on_land, is_base_defense,
+ avoid_others)
+ return position
+
+ def _find_random_position(self, min_range: int, max_range: int,
+ on_ground: bool, is_base_defense: bool,
+ avoid_others: bool) -> Optional[Point]:
+ """
+ Find a valid ground object location
+ :param on_ground: Whether it should be on ground or on sea (True = on
+ ground)
+ :param theater: Theater object
+ :param min_range: Minimal range from point
+ :param max_range: Max range from point
+ :param is_base_defense: True if the location is for base defense.
+ :return:
+ """
+ near = self.control_point.position
+ others = self.control_point.ground_objects
+
+ def is_valid(point: Optional[Point]) -> bool:
+ if point is None:
+ return False
+
+ if on_ground and not self.game.theater.is_on_land(point):
+ return False
+ elif not on_ground and not self.game.theater.is_in_sea(point):
+ return False
+
+ if avoid_others:
+ for other in others:
+ if other.position.distance_to_point(point) < 10000:
+ return False
+
+ if is_base_defense:
+ # If it's a base defense we don't care how close it is to other
+ # points.
+ return True
+
+ # Else verify that it's not too close to another control point.
+ for control_point in self.game.theater.controlpoints:
+ if control_point != self.control_point:
+ if control_point.position.distance_to_point(point) < 30000:
+ return False
+ for ground_obj in control_point.ground_objects:
+ if ground_obj.position.distance_to_point(point) < 10000:
+ return False
+ return True
+
+ for _ in range(300):
+ # Check if on land or sea
+ p = near.random_point_within(max_range, min_range)
+ if is_valid(p):
+ return p
+ return None
+
+
class ControlPointGroundObjectGenerator:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
- self.preset_locations = PresetLocationFinder.compute_possible_locations(game.theater.terrain.name, control_point.full_name)
+ self.location_finder = LocationFinder(game, control_point)
@property
def faction_name(self) -> str:
@@ -205,11 +355,9 @@ class ControlPointGroundObjectGenerator:
self.generate_ship()
def generate_ship(self) -> None:
- point = find_location(False, self.control_point.position,
- self.game.theater, 5000, 40000, [], False)
+ point = self.location_finder.location_for(
+ LocationType.OffshoreStrikeTarget)
if point is None:
- logging.error(
- f"Could not find point for {self.control_point}'s navy")
return
group_id = self.game.next_group_id()
@@ -223,26 +371,10 @@ class ControlPointGroundObjectGenerator:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
- def pick_preset_location(self, offshore=False) -> Optional[PresetLocation]:
- """
- Return a preset location if any is setup and still available for this point
- @:param offshore Whether this should be an offshore location
- @:return The preset location if found; None if it couldn't be found
- """
- if offshore:
- if len(self.preset_locations.offshore_locations) > 0:
- location = random.choice(self.preset_locations.offshore_locations)
- self.preset_locations.offshore_locations.remove(location)
- logging.info("Picked a preset offshore location")
- return location
- else:
- if len(self.preset_locations.ashore_locations) > 0:
- location = random.choice(self.preset_locations.ashore_locations)
- self.preset_locations.ashore_locations.remove(location)
- logging.info("Picked a preset ashore location")
- return location
- logging.info("No preset location found")
- return None
+
+class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
+ def generate(self) -> bool:
+ return True
class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
@@ -299,6 +431,7 @@ class BaseDefenseGenerator:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
+ self.location_finder = LocationFinder(game, control_point)
@property
def faction_name(self) -> str:
@@ -317,10 +450,8 @@ class BaseDefenseGenerator:
self.generate_base_defenses()
def generate_ewr(self) -> None:
- position = self._find_location()
+ position = self.location_finder.location_for(LocationType.Ewr)
if position is None:
- logging.error("Could not find position for "
- f"{self.control_point} EWR")
return
group_id = self.game.next_group_id()
@@ -350,10 +481,8 @@ class BaseDefenseGenerator:
self.generate_garrison()
def generate_garrison(self) -> None:
- position = self._find_location()
+ position = self.location_finder.location_for(LocationType.Garrison)
if position is None:
- logging.error("Could not find position for "
- f"{self.control_point} garrison")
return
group_id = self.game.next_group_id()
@@ -368,10 +497,9 @@ class BaseDefenseGenerator:
self.control_point.base_defenses.append(g)
def generate_sam(self) -> None:
- position = self._find_location()
+ position = self.location_finder.location_for(
+ LocationType.BaseAirDefense)
if position is None:
- logging.error("Could not find position for "
- f"{self.control_point} SAM")
return
group_id = self.game.next_group_id()
@@ -385,10 +513,9 @@ class BaseDefenseGenerator:
self.control_point.base_defenses.append(g)
def generate_shorad(self) -> None:
- position = self._find_location()
+ position = self.location_finder.location_for(
+ LocationType.BaseAirDefense)
if position is None:
- logging.error("Could not find position for "
- f"{self.control_point} SHORAD")
return
group_id = self.game.next_group_id()
@@ -401,20 +528,6 @@ class BaseDefenseGenerator:
g.groups.append(group)
self.control_point.base_defenses.append(g)
- def _find_location(self) -> Optional[Point]:
- position = find_location(True, self.control_point.position,
- self.game.theater, 400, 3200, [], True)
-
- # Retry once, searching a bit further (On some big airbase, 3200 is too short (Ex : Incirlik))
- # But searching farther on every base would be problematic, as some base defense units
- # would end up very far away from small airfields.
- # (I know it's not good for performance, but this is only done on campaign generation)
- # TODO : Make the whole process less stupid with preset possible positions for each airbase
- if position is None:
- position = find_location(True, self.control_point.position,
- self.game.theater, 3200, 4800, [], True)
- return position
-
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__(self, game: Game, control_point: ControlPoint,
@@ -442,15 +555,31 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
# Always generate at least one AA point.
self.generate_aa_site()
+ skip_sams = self.generate_required_aa()
+
# And between 2 and 7 other objectives.
amount = random.randrange(2, 7)
for i in range(amount):
# 1 in 4 additional objectives are AA.
if random.randint(0, 3) == 0:
- self.generate_aa_site()
+ if skip_sams > 0:
+ skip_sams -= 1
+ else:
+ self.generate_aa_site()
else:
self.generate_ground_point()
+ def generate_required_aa(self) -> int:
+ """Generates the AA sites that are required by the campaign.
+
+ Returns:
+ The number of AA sites that were generated.
+ """
+ sams = self.control_point.preset_locations.required_sams
+ for position in sams:
+ self.generate_aa_at(position)
+ return len(sams)
+
def generate_ground_point(self) -> None:
try:
category = random.choice(self.faction.building_set)
@@ -461,23 +590,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
- offshore = category == "oil"
+ if category == "oil":
+ location_type = LocationType.OffshoreStrikeTarget
+ else:
+ location_type = LocationType.StrikeTarget
# Pick from preset locations
- location = self.pick_preset_location(offshore)
-
- # Else try the old algorithm
- if location is None:
- point = find_location(not offshore,
- self.control_point.position,
- self.game.theater, 10000, 40000,
- self.control_point.ground_objects)
- else:
- point = location.position
-
+ point = self.location_finder.location_for(location_type)
if point is None:
- logging.error(
- f"Could not find point for {obj_name} at {self.control_point}")
return
object_id = 0
@@ -495,24 +615,12 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
def generate_aa_site(self) -> None:
- obj_name = namegen.random_objective_name()
-
- # Pick from preset locations
- location = self.pick_preset_location(False)
-
- # If no preset location, then try the old algorithm
- if location is None:
- position = find_location(True, self.control_point.position,
- self.game.theater, 10000, 40000,
- self.control_point.ground_objects)
- else:
- position = location.position
-
+ position = self.location_finder.location_for(LocationType.Sam)
if position is None:
- logging.error(
- f"Could not find point for {obj_name} at {self.control_point}")
return
+ self.generate_aa_at(position)
+ def generate_aa_at(self, position: Point) -> None:
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
@@ -527,22 +635,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.generate_missile_site()
def generate_missile_site(self) -> None:
-
- # Pick from preset locations
- location = self.pick_preset_location(False)
-
- # If no preset location, then try the old algorithm
- if location is None:
- position = find_location(True, self.control_point.position,
- self.game.theater, 2500, 40000,
- [], False)
- else:
- position = location.position
-
-
+ position = self.location_finder.location_for(LocationType.MissileSite)
if position is None:
- logging.info(
- f"Could not find point for {self.control_point} missile site")
return
group_id = self.game.next_group_id()
@@ -577,72 +671,9 @@ class GroundObjectGenerator:
generator = CarrierGroundObjectGenerator(self.game, control_point)
elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator(self.game, control_point)
+ elif isinstance(control_point, OffMapSpawn):
+ generator = NoOpGroundObjectGenerator(self.game, control_point)
else:
generator = AirbaseGroundObjectGenerator(self.game, control_point,
self.templates)
return generator.generate()
-
-
-# TODO: https://stackoverflow.com/a/19482012/632035
-# A lot of the time spent on mission generation is spent in this function since
-# just randomly guess up to 1800 times and often fail. This is particularly
-# problematic while trying to find placement for navies in Nevada.
-def find_location(on_ground: bool, near: Point, theater: ConflictTheater,
- min_range: int, max_range: int,
- others: List[TheaterGroundObject],
- is_base_defense: bool = False) -> Optional[Point]:
- """
- Find a valid ground object location
- :param on_ground: Whether it should be on ground or on sea (True = on
- ground)
- :param near: Point
- :param theater: Theater object
- :param min_range: Minimal range from point
- :param max_range: Max range from point
- :param others: Other already existing ground objects
- :param is_base_defense: True if the location is for base defense.
- :return:
- """
- point = None
- for _ in range(300):
-
- # Check if on land or sea
- p = near.random_point_within(max_range, min_range)
- if on_ground and theater.is_on_land(p):
- point = p
- elif not on_ground and theater.is_in_sea(p):
- point = p
-
- if point:
- for angle in range(0, 360, 45):
- p = point.point_from_heading(angle, 2500)
- if on_ground and not theater.is_on_land(p):
- point = None
- break
- elif not on_ground and not theater.is_in_sea(p):
- point = None
- break
- if point:
- for other in others:
- if other.position.distance_to_point(point) < 10000:
- point = None
- break
-
- if point:
- for control_point in theater.controlpoints:
- if is_base_defense:
- break
- if control_point.position != near:
- if point is None:
- break
- if control_point.position.distance_to_point(point) < 30000:
- point = None
- break
- for ground_obj in control_point.ground_objects:
- if ground_obj.position.distance_to_point(point) < 10000:
- point = None
- break
-
- if point:
- return point
- return None
diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py
index c267a0eb..7f3f44a9 100644
--- a/game/theater/theatergroundobject.py
+++ b/game/theater/theatergroundobject.py
@@ -243,8 +243,8 @@ class BaseDefenseGroundObject(TheaterGroundObject):
# TODO: Differentiate types.
-# This type gets used both for AA sites (SAM, AAA, or SHORAD) but also for the
-# armor garrisons at airbases. These should each be split into their own types.
+# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
+# be split into their own types.
class SamGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, for_airbase: bool) -> None:
diff --git a/game/utils.py b/game/utils.py
index 44652472..58bc5018 100644
--- a/game/utils.py
+++ b/game/utils.py
@@ -12,3 +12,7 @@ def meter_to_nm(value_in_meter: float) -> int:
def nm_to_meter(value_in_nm: float) -> int:
return int(value_in_nm * 1852)
+
+
+def knots_to_kph(knots: float) -> int:
+ return int(knots * 1.852)
diff --git a/game/weather.py b/game/weather.py
index d6775614..e8efd6e7 100644
--- a/game/weather.py
+++ b/game/weather.py
@@ -10,7 +10,7 @@ from typing import Optional
from dcs.weather import Weather as PydcsWeather, Wind
from game.settings import Settings
-from theater import ConflictTheater
+from game.theater import ConflictTheater
class TimeOfDay(Enum):
diff --git a/gen/aircraft.py b/gen/aircraft.py
index 9eccfb17..f3690915 100644
--- a/gen/aircraft.py
+++ b/gen/aircraft.py
@@ -70,7 +70,13 @@ from dcs.unittype import FlyingType, UnitType
from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings
-from game.utils import nm_to_meter
+from game.theater.controlpoint import (
+ ControlPoint,
+ ControlPointType,
+ OffMapSpawn,
+)
+from game.theater.theatergroundobject import TheaterGroundObject
+from game.utils import knots_to_kph, nm_to_meter
from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit
@@ -83,8 +89,6 @@ from gen.flights.flight import (
)
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData
-from theater import TheaterGroundObject
-from game.theater.controlpoint import ControlPoint, ControlPointType
from .conflictgen import Conflict
from .flights.flightplan import (
CasFlightPlan,
@@ -92,7 +96,7 @@ from .flights.flightplan import (
PatrollingFlightPlan,
SweepFlightPlan,
)
-from .flights.traveltime import TotEstimator
+from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen
from .runways import RunwayAssigner
@@ -691,6 +695,18 @@ class AircraftConflictGenerator:
return StartType.Cold
return StartType.Warm
+ def determine_runway(self, cp: ControlPoint, dynamic_runways) -> RunwayData:
+ fallback = RunwayData(cp.full_name, runway_heading=0, runway_name="")
+ if cp.cptype == ControlPointType.AIRBASE:
+ assigner = RunwayAssigner(self.game.conditions)
+ return assigner.get_preferred_runway(cp.airport)
+ elif cp.is_fleet:
+ return dynamic_runways.get(cp.name, fallback)
+ else:
+ logging.warning(
+ f"Unhandled departure/arrival control point: {cp.cptype}")
+ return fallback
+
def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
package: Package, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
@@ -748,19 +764,9 @@ class AircraftConflictGenerator:
channel = self.get_intra_flight_channel(unit_type)
group.set_frequency(channel.mhz)
- # TODO: Support for different departure/arrival airfields.
- cp = flight.from_cp
- fallback_runway = RunwayData(cp.full_name, runway_heading=0,
- runway_name="")
- if cp.cptype == ControlPointType.AIRBASE:
- assigner = RunwayAssigner(self.game.conditions)
- departure_runway = assigner.get_preferred_runway(
- flight.from_cp.airport)
- elif cp.is_fleet:
- departure_runway = dynamic_runways.get(cp.name, fallback_runway)
- else:
- logging.warning(f"Unhandled departure control point: {cp.cptype}")
- departure_runway = fallback_runway
+ divert = None
+ if flight.divert is not None:
+ divert = self.determine_runway(flight.divert, dynamic_runways)
self.flights.append(FlightData(
package=package,
@@ -770,10 +776,9 @@ class AircraftConflictGenerator:
friendly=flight.from_cp.captured,
# Set later.
departure_delay=timedelta(),
- departure=departure_runway,
- arrival=departure_runway,
- # TODO: Support for divert airfields.
- divert=None,
+ departure=self.determine_runway(flight.departure, dynamic_runways),
+ arrival=self.determine_runway(flight.arrival, dynamic_runways),
+ divert=divert,
# Waypoints are added later, after they've had their TOTs set.
waypoints=[],
intra_flight_channel=channel
@@ -804,31 +809,37 @@ class AircraftConflictGenerator:
group_size=count,
parking_slots=None)
- def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, at: Point) -> FlyingGroup:
- assert count > 0
+ def _generate_inflight(self, name: str, side: Country, flight: Flight,
+ origin: ControlPoint) -> FlyingGroup:
+ assert flight.count > 0
+ at = origin.position
- if unit_type in helicopters.helicopter_map.values():
+ alt_type = "RADIO"
+ if isinstance(origin, OffMapSpawn):
+ alt = flight.flight_plan.waypoints[0].alt
+ alt_type = flight.flight_plan.waypoints[0].alt_type
+ elif flight.unit_type in helicopters.helicopter_map.values():
alt = WARM_START_HELI_ALT
- speed = WARM_START_HELI_AIRSPEED
else:
alt = WARM_START_ALTITUDE
- speed = WARM_START_AIRSPEED
+
+ speed = knots_to_kph(GroundSpeed.for_flight(flight, alt))
pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000))
- logging.info("airgen: {} for {} at {} at {}".format(unit_type, side.id, alt, speed))
+ logging.info("airgen: {} for {} at {} at {}".format(flight.unit_type, side.id, alt, speed))
group = self.m.flight_group(
country=side,
name=name,
- aircraft_type=unit_type,
+ aircraft_type=flight.unit_type,
airport=None,
position=pos,
altitude=alt,
speed=speed,
maintask=None,
- group_size=count)
+ group_size=flight.count)
- group.points[0].alt_type = "RADIO"
+ group.points[0].alt_type = alt_type
return group
def _generate_at_group(self, name: str, side: Country,
@@ -974,9 +985,8 @@ class AircraftConflictGenerator:
group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country,
- unit_type=flight.unit_type,
- count=flight.count,
- at=cp.position)
+ flight=flight,
+ origin=cp)
elif cp.is_fleet:
group_name = cp.get_carrier_group_name()
group = self._generate_at_group(
@@ -1002,9 +1012,8 @@ class AircraftConflictGenerator:
group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country,
- unit_type=flight.unit_type,
- count=flight.count,
- at=cp.position)
+ flight=flight,
+ origin=cp)
group.points[0].alt = 1500
return group
diff --git a/gen/conflictgen.py b/gen/conflictgen.py
index 6a5a8e07..35be5956 100644
--- a/gen/conflictgen.py
+++ b/gen/conflictgen.py
@@ -5,7 +5,8 @@ from typing import Tuple
from dcs.country import Country
from dcs.mapping import Point
-from theater import ConflictTheater, ControlPoint, FrontLine
+from game.theater.conflicttheater import ConflictTheater, FrontLine
+from game.theater.controlpoint import ControlPoint
AIR_DISTANCE = 40000
diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py
index ee458908..182e0455 100644
--- a/gen/flights/ai_flight_planner.py
+++ b/gen/flights/ai_flight_planner.py
@@ -16,11 +16,24 @@ from typing import (
Type,
)
-from dcs.unittype import FlyingType, UnitType
+from dcs.unittype import FlyingType
from game import db
from game.data.radar_db import UNITS_WITH_RADAR
from game.infos.information import Information
+from game.theater import (
+ ControlPoint,
+ FrontLine,
+ MissionTarget,
+ OffMapSpawn,
+ SamGroundObject,
+ TheaterGroundObject,
+)
+# Avoid importing some types that cause circular imports unless type checking.
+from game.theater.theatergroundobject import (
+ EwrGroundObject,
+ NavalGroundObject, VehicleGroupGroundObject,
+)
from game.utils import nm_to_meter
from gen import Conflict
from gen.ato import Package
@@ -46,19 +59,6 @@ from gen.flights.flight import (
)
from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator
-from theater import (
- ControlPoint,
- FrontLine,
- MissionTarget,
- TheaterGroundObject,
- SamGroundObject,
-)
-
-# Avoid importing some types that cause circular imports unless type checking.
-from game.theater.theatergroundobject import (
- EwrGroundObject,
- NavalGroundObject, VehicleGroupGroundObject,
-)
if TYPE_CHECKING:
from game import Game
@@ -119,7 +119,7 @@ class AircraftAllocator:
def find_aircraft_for_flight(
self, flight: ProposedFlight
- ) -> Optional[Tuple[ControlPoint, UnitType]]:
+ ) -> Optional[Tuple[ControlPoint, FlyingType]]:
"""Finds aircraft suitable for the given mission.
Searches for aircraft capable of performing the given mission within the
@@ -190,7 +190,7 @@ class AircraftAllocator:
def find_aircraft_of_type(
self, flight: ProposedFlight, types: List[Type[FlyingType]],
- ) -> Optional[Tuple[ControlPoint, UnitType]]:
+ ) -> Optional[Tuple[ControlPoint, FlyingType]]:
airfields_in_range = self.closest_airfields.airfields_within(
flight.max_distance
)
@@ -214,6 +214,8 @@ class PackageBuilder:
global_inventory: GlobalAircraftInventory,
is_player: bool,
start_type: str) -> None:
+ self.closest_airfields = closest_airfields
+ self.is_player = is_player
self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
is_player)
@@ -232,11 +234,32 @@ class PackageBuilder:
if assignment is None:
return False
airfield, aircraft = assignment
- flight = Flight(self.package, aircraft, plan.num_aircraft, airfield,
- plan.task, self.start_type)
+ if isinstance(airfield, OffMapSpawn):
+ start_type = "In Flight"
+ else:
+ start_type = self.start_type
+
+ flight = Flight(self.package, aircraft, plan.num_aircraft, plan.task,
+ start_type, departure=airfield, arrival=airfield,
+ divert=self.find_divert_field(aircraft, airfield))
self.package.add_flight(flight)
return True
+ def find_divert_field(self, aircraft: FlyingType,
+ arrival: ControlPoint) -> Optional[ControlPoint]:
+ divert_limit = nm_to_meter(150)
+ for airfield in self.closest_airfields.airfields_within(divert_limit):
+ if airfield.captured != self.is_player:
+ continue
+ if airfield == arrival:
+ continue
+ if not airfield.can_land(aircraft):
+ continue
+ if isinstance(airfield, OffMapSpawn):
+ continue
+ return airfield
+ return None
+
def build(self) -> Package:
"""Returns the built package."""
return self.package
@@ -406,6 +429,9 @@ class ObjectiveFinder:
CP.
"""
for cp in self.friendly_control_points():
+ if isinstance(cp, OffMapSpawn):
+ # Off-map spawn locations don't need protection.
+ continue
airfields_in_proximity = self.closest_airfields_to(cp)
airfields_in_threat_range = airfields_in_proximity.airfields_within(
self.AIRFIELD_THREAT_RANGE
diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py
index a6045dde..5bba28db 100644
--- a/gen/flights/closestairfields.py
+++ b/gen/flights/closestairfields.py
@@ -1,7 +1,7 @@
"""Objective adjacency lists."""
from typing import Dict, Iterator, List, Optional
-from theater import ConflictTheater, ControlPoint, MissionTarget
+from game.theater import ConflictTheater, ControlPoint, MissionTarget
class ClosestAirfields:
diff --git a/gen/flights/flight.py b/gen/flights/flight.py
index 2b5e35ea..276a6396 100644
--- a/gen/flights/flight.py
+++ b/gen/flights/flight.py
@@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import timedelta
from enum import Enum
-from typing import Dict, List, Optional, TYPE_CHECKING
+from typing import Dict, List, Optional, TYPE_CHECKING, Type
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
@@ -65,6 +65,7 @@ class FlightWaypointType(Enum):
INGRESS_DEAD = 20
INGRESS_SWEEP = 21
INGRESS_BAI = 22
+ DIVERT = 23
class FlightWaypoint:
@@ -132,13 +133,16 @@ class FlightWaypoint:
class Flight:
- def __init__(self, package: Package, unit_type: FlyingType, count: int,
- from_cp: ControlPoint, flight_type: FlightType,
- start_type: str) -> None:
+ def __init__(self, package: Package, unit_type: Type[FlyingType],
+ count: int, flight_type: FlightType, start_type: str,
+ departure: ControlPoint, arrival: ControlPoint,
+ divert: Optional[ControlPoint]) -> None:
self.package = package
self.unit_type = unit_type
self.count = count
- self.from_cp = from_cp
+ self.departure = departure
+ self.arrival = arrival
+ self.divert = divert
self.flight_type = flight_type
# TODO: Replace with FlightPlan.
self.targets: List[MissionTarget] = []
@@ -157,6 +161,10 @@ class Flight:
custom_waypoints=[]
)
+ @property
+ def from_cp(self) -> ControlPoint:
+ return self.departure
+
@property
def points(self) -> List[FlightWaypoint]:
return self.flight_plan.waypoints[1:]
diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py
index 918861e2..d8758e32 100644
--- a/gen/flights/flightplan.py
+++ b/gen/flights/flightplan.py
@@ -7,20 +7,19 @@ generating the waypoints for the mission.
"""
from __future__ import annotations
-import math
-from datetime import timedelta
-from functools import cached_property
import logging
+import math
import random
from dataclasses import dataclass
+from datetime import timedelta
+from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.mapping import Point
from dcs.unit import Unit
from game.data.doctrine import Doctrine
-from game.utils import nm_to_meter
-from theater import (
+from game.theater import (
ControlPoint,
FrontLine,
MissionTarget,
@@ -28,6 +27,7 @@ from theater import (
TheaterGroundObject,
)
from game.theater.theatergroundobject import EwrGroundObject
+from game.utils import nm_to_meter
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime
@@ -68,6 +68,10 @@ class FlightPlan:
@property
def waypoints(self) -> List[FlightWaypoint]:
"""A list of all waypoints in the flight plan, in order."""
+ return list(self.iter_waypoints())
+
+ def iter_waypoints(self) -> Iterator[FlightWaypoint]:
+ """Iterates over all waypoints in the flight plan, in order."""
raise NotImplementedError
@property
@@ -166,8 +170,7 @@ class FlightPlan:
class LoiterFlightPlan(FlightPlan):
hold: FlightWaypoint
- @property
- def waypoints(self) -> List[FlightWaypoint]:
+ def iter_waypoints(self) -> Iterator[FlightWaypoint]:
raise NotImplementedError
@property
@@ -193,8 +196,7 @@ class FormationFlightPlan(LoiterFlightPlan):
join: FlightWaypoint
split: FlightWaypoint
- @property
- def waypoints(self) -> List[FlightWaypoint]:
+ def iter_waypoints(self) -> Iterator[FlightWaypoint]:
raise NotImplementedError
@property
@@ -295,8 +297,7 @@ class PatrollingFlightPlan(FlightPlan):
return self.patrol_end_time
return None
- @property
- def waypoints(self) -> List[FlightWaypoint]:
+ def iter_waypoints(self) -> Iterator[FlightWaypoint]:
raise NotImplementedError
@property
@@ -312,15 +313,17 @@ class PatrollingFlightPlan(FlightPlan):
class BarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
land: FlightWaypoint
+ divert: Optional[FlightWaypoint]
- @property
- def waypoints(self) -> List[FlightWaypoint]:
- return [
+ def iter_waypoints(self) -> Iterator[FlightWaypoint]:
+ yield from [
self.takeoff,
self.patrol_start,
self.patrol_end,
self.land,
]
+ if self.divert is not None:
+ yield self.divert
@dataclass(frozen=True)
@@ -328,16 +331,18 @@ class CasFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
target: FlightWaypoint
land: FlightWaypoint
+ divert: Optional[FlightWaypoint]
- @property
- def waypoints(self) -> List[FlightWaypoint]:
- return [
+ def iter_waypoints(self) -> Iterator[FlightWaypoint]:
+ yield from [
self.takeoff,
self.patrol_start,
self.target,
self.patrol_end,
self.land,
]
+ if self.divert is not None:
+ yield self.divert
def request_escort_at(self) -> Optional[FlightWaypoint]:
return self.patrol_start
@@ -350,16 +355,18 @@ class CasFlightPlan(PatrollingFlightPlan):
class TarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
land: FlightWaypoint
+ divert: Optional[FlightWaypoint]
lead_time: timedelta
- @property
- def waypoints(self) -> List[FlightWaypoint]:
- return [
+ def iter_waypoints(self) -> Iterator[FlightWaypoint]:
+ yield from [
self.takeoff,
self.patrol_start,
self.patrol_end,
self.land,
]
+ if self.divert is not None:
+ yield self.divert
@property
def tot_offset(self) -> timedelta:
@@ -386,10 +393,6 @@ class TarCapFlightPlan(PatrollingFlightPlan):
return super().patrol_end_time
-# TODO: Remove when breaking save compat.
-FrontLineCapFlightPlan = TarCapFlightPlan
-
-
@dataclass(frozen=True)
class StrikeFlightPlan(FormationFlightPlan):
takeoff: FlightWaypoint
@@ -400,19 +403,23 @@ class StrikeFlightPlan(FormationFlightPlan):
egress: FlightWaypoint
split: FlightWaypoint
land: FlightWaypoint
+ divert: Optional[FlightWaypoint]
- @property
- def waypoints(self) -> List[FlightWaypoint]:
- return [
+ def iter_waypoints(self) -> Iterator[FlightWaypoint]:
+ yield from [
self.takeoff,
self.hold,
self.join,
self.ingress
- ] + self.targets + [
+ ]
+ yield from self.targets
+ yield from[
self.egress,
self.split,
self.land,
]
+ if self.divert is not None:
+ yield self.divert
@property
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
@@ -511,17 +518,19 @@ class SweepFlightPlan(LoiterFlightPlan):
sweep_start: FlightWaypoint
sweep_end: FlightWaypoint
land: FlightWaypoint
+ divert: Optional[FlightWaypoint]
lead_time: timedelta
- @property
- def waypoints(self) -> List[FlightWaypoint]:
- return [
+ def iter_waypoints(self) -> Iterator[FlightWaypoint]:
+ yield from [
self.takeoff,
self.hold,
self.sweep_start,
self.sweep_end,
self.land,
]
+ if self.divert is not None:
+ yield self.divert
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
@@ -567,9 +576,8 @@ class SweepFlightPlan(LoiterFlightPlan):
class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint]
- @property
- def waypoints(self) -> List[FlightWaypoint]:
- return self.custom_waypoints
+ def iter_waypoints(self) -> Iterator[FlightWaypoint]:
+ yield from self.custom_waypoints
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
@@ -774,10 +782,11 @@ class FlightPlanBuilder:
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cap_duration,
- takeoff=builder.takeoff(flight.from_cp),
+ takeoff=builder.takeoff(flight.departure),
patrol_start=start,
patrol_end=end,
- land=builder.land(flight.from_cp)
+ land=builder.land(flight.arrival),
+ divert=builder.divert(flight.divert)
)
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
@@ -800,11 +809,12 @@ class FlightPlanBuilder:
package=self.package,
flight=flight,
lead_time=timedelta(minutes=5),
- takeoff=builder.takeoff(flight.from_cp),
+ takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)),
sweep_start=start,
sweep_end=end,
- land=builder.land(flight.from_cp)
+ land=builder.land(flight.arrival),
+ divert=builder.divert(flight.divert)
)
def racetrack_for_objective(self,
@@ -900,10 +910,11 @@ class FlightPlanBuilder:
# requests an escort the CAP flight will remain on station for the
# duration of the escorted mission, or until it is winchester/bingo.
patrol_duration=self.doctrine.cap_duration,
- takeoff=builder.takeoff(flight.from_cp),
+ takeoff=builder.takeoff(flight.departure),
patrol_start=start,
patrol_end=end,
- land=builder.land(flight.from_cp)
+ land=builder.land(flight.arrival),
+ divert=builder.divert(flight.divert)
)
def generate_dead(self, flight: Flight,
@@ -965,14 +976,15 @@ class FlightPlanBuilder:
return StrikeFlightPlan(
package=self.package,
flight=flight,
- takeoff=builder.takeoff(flight.from_cp),
+ takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join),
ingress=ingress,
targets=[target],
egress=egress,
split=builder.split(self.package.waypoints.split),
- land=builder.land(flight.from_cp)
+ land=builder.land(flight.arrival),
+ divert=builder.divert(flight.divert)
)
def generate_cas(self, flight: Flight) -> CasFlightPlan:
@@ -999,11 +1011,12 @@ class FlightPlanBuilder:
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cas_duration,
- takeoff=builder.takeoff(flight.from_cp),
+ takeoff=builder.takeoff(flight.departure),
patrol_start=builder.ingress_cas(ingress, location),
target=builder.cas(center),
patrol_end=builder.egress(egress, location),
- land=builder.land(flight.from_cp)
+ land=builder.land(flight.arrival),
+ divert=builder.divert(flight.divert)
)
@staticmethod
@@ -1030,7 +1043,7 @@ class FlightPlanBuilder:
def _hold_point(self, flight: Flight) -> Point:
assert self.package.waypoints is not None
- origin = flight.from_cp.position
+ origin = flight.departure.position
target = self.package.target.position
join = self.package.waypoints.join
origin_to_target = origin.distance_to_point(target)
@@ -1118,14 +1131,15 @@ class FlightPlanBuilder:
return StrikeFlightPlan(
package=self.package,
flight=flight,
- takeoff=builder.takeoff(flight.from_cp),
+ takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join),
ingress=ingress,
targets=target_waypoints,
egress=builder.egress(self.package.waypoints.egress, location),
split=builder.split(self.package.waypoints.split),
- land=builder.land(flight.from_cp)
+ land=builder.land(flight.arrival),
+ divert=builder.divert(flight.divert)
)
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
@@ -1201,7 +1215,7 @@ class FlightPlanBuilder:
)
for airfield in cache.closest_airfields:
for flight in self.package.flights:
- if flight.from_cp == airfield:
+ if flight.departure == airfield:
return airfield
raise RuntimeError(
"Could not find any airfield assigned to this package"
diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py
index 742dfce3..7cc45069 100644
--- a/gen/flights/traveltime.py
+++ b/gen/flights/traveltime.py
@@ -45,20 +45,21 @@ class GroundSpeed:
return int(cls.from_mach(mach, altitude)) # knots
@staticmethod
- def from_mach(mach: float, altitude: int) -> float:
+ def from_mach(mach: float, altitude_m: int) -> float:
"""Returns the ground speed in knots for the given mach and altitude.
Args:
mach: The mach number to convert to ground speed.
- altitude: The altitude in feet.
+ altitude_m: The altitude in meters.
Returns:
The ground speed corresponding to the given altitude and mach number
in knots.
"""
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
- if altitude <= 36152:
- temperature_f = 59 - 0.00356 * altitude
+ altitude_ft = altitude_m * 3.28084
+ if altitude_ft <= 36152:
+ temperature_f = 59 - 0.00356 * altitude_ft
else:
# There's another formula for altitudes over 82k feet, but we better
# not be planning waypoints that high...
diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py
index f220ebb4..74731929 100644
--- a/gen/flights/waypointbuilder.py
+++ b/gen/flights/waypointbuilder.py
@@ -8,11 +8,14 @@ from dcs.unit import Unit
from dcs.unitgroup import VehicleGroup
from game.data.doctrine import Doctrine
-from game.utils import nm_to_meter
+from game.theater import (
+ ControlPoint,
+ MissionTarget,
+ OffMapSpawn,
+ TheaterGroundObject,
+)
from game.weather import Conditions
-from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType
-from ..runways import RunwayAssigner
@dataclass(frozen=True)
@@ -34,8 +37,7 @@ class WaypointBuilder:
def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False)
- @staticmethod
- def takeoff(departure: ControlPoint) -> FlightWaypoint:
+ def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
Note that the takeoff waypoint will automatically be created by pydcs
@@ -46,36 +48,93 @@ class WaypointBuilder:
departure: Departure airfield or carrier.
"""
position = departure.position
- waypoint = FlightWaypoint(
- FlightWaypointType.TAKEOFF,
- position.x,
- position.y,
- 0
- )
- waypoint.name = "TAKEOFF"
- waypoint.alt_type = "RADIO"
- waypoint.description = "Takeoff"
- waypoint.pretty_name = "Takeoff"
+ if isinstance(departure, OffMapSpawn):
+ waypoint = FlightWaypoint(
+ FlightWaypointType.NAV,
+ position.x,
+ position.y,
+ 500 if self.is_helo else self.doctrine.rendezvous_altitude
+ )
+ waypoint.name = "NAV"
+ waypoint.alt_type = "BARO"
+ waypoint.description = "Enter theater"
+ waypoint.pretty_name = "Enter theater"
+ else:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.TAKEOFF,
+ position.x,
+ position.y,
+ 0
+ )
+ waypoint.name = "TAKEOFF"
+ waypoint.alt_type = "RADIO"
+ waypoint.description = "Takeoff"
+ waypoint.pretty_name = "Takeoff"
return waypoint
- @staticmethod
- def land(arrival: ControlPoint) -> FlightWaypoint:
+ def land(self, arrival: ControlPoint) -> FlightWaypoint:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
arrival: Arrival airfield or carrier.
"""
position = arrival.position
+ if isinstance(arrival, OffMapSpawn):
+ waypoint = FlightWaypoint(
+ FlightWaypointType.NAV,
+ position.x,
+ position.y,
+ 500 if self.is_helo else self.doctrine.rendezvous_altitude
+ )
+ waypoint.name = "NAV"
+ waypoint.alt_type = "BARO"
+ waypoint.description = "Exit theater"
+ waypoint.pretty_name = "Exit theater"
+ else:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.LANDING_POINT,
+ position.x,
+ position.y,
+ 0
+ )
+ waypoint.name = "LANDING"
+ waypoint.alt_type = "RADIO"
+ waypoint.description = "Land"
+ waypoint.pretty_name = "Land"
+ return waypoint
+
+ def divert(self,
+ divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
+ """Create divert waypoint for the given arrival airfield or carrier.
+
+ Args:
+ divert: Divert airfield or carrier.
+ """
+ if divert is None:
+ return None
+
+ position = divert.position
+ if isinstance(divert, OffMapSpawn):
+ if self.is_helo:
+ altitude = 500
+ else:
+ altitude = self.doctrine.rendezvous_altitude
+ altitude_type = "BARO"
+ else:
+ altitude = 0
+ altitude_type = "RADIO"
+
waypoint = FlightWaypoint(
- FlightWaypointType.LANDING_POINT,
+ FlightWaypointType.DIVERT,
position.x,
position.y,
- 0
+ altitude
)
- waypoint.name = "LANDING"
- waypoint.alt_type = "RADIO"
- waypoint.description = "Land"
- waypoint.pretty_name = "Land"
+ waypoint.alt_type = altitude_type
+ waypoint.name = "DIVERT"
+ waypoint.description = "Divert"
+ waypoint.pretty_name = "Divert"
+ waypoint.only_for_player = True
return waypoint
def hold(self, position: Point) -> FlightWaypoint:
diff --git a/gen/ground_forces/ai_ground_planner.py b/gen/ground_forces/ai_ground_planner.py
index db1deb03..b0f14df4 100644
--- a/gen/ground_forces/ai_ground_planner.py
+++ b/gen/ground_forces/ai_ground_planner.py
@@ -2,12 +2,12 @@ import random
from enum import Enum
from typing import Dict, List
-from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
from dcs.unittype import VehicleType
+from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
import pydcs_extensions.frenchpack.frenchpack as frenchpack
+from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance
-from theater import ControlPoint
TYPE_TANKS = [
Armor.MBT_T_55,
diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py
index edd58e6d..09385c78 100644
--- a/gen/groundobjectsgen.py
+++ b/gen/groundobjectsgen.py
@@ -20,14 +20,14 @@ from dcs.task import (
EPLRS,
OptAlarmState,
)
-from dcs.unit import Ship, Vehicle, Unit
+from dcs.unit import Ship, Unit, Vehicle
from dcs.unitgroup import Group, ShipGroup, StaticGroup
from dcs.unittype import StaticType, UnitType
from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name
-from theater import ControlPoint, TheaterGroundObject
+from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
BuildingGroundObject, CarrierGroundObject,
GenericCarrierGroundObject,
diff --git a/gen/locations/preset_location_finder.py b/gen/locations/preset_location_finder.py
index 41386d90..4df32466 100644
--- a/gen/locations/preset_location_finder.py
+++ b/gen/locations/preset_location_finder.py
@@ -8,7 +8,7 @@ from gen.locations.preset_control_point_locations import PresetControlPointLocat
from gen.locations.preset_locations import PresetLocation
-class PresetLocationFinder:
+class MizDataLocationFinder:
@staticmethod
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:
diff --git a/gen/radios.py b/gen/radios.py
index c2180fe3..87b8661f 100644
--- a/gen/radios.py
+++ b/gen/radios.py
@@ -134,7 +134,7 @@ RADIOS: List[Radio] = [
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
# MiG-21bis
- Radio("RSIU-5V", MHz(100), MHz(150), step=MHz(1)),
+ Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
# Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
diff --git a/gen/runways.py b/gen/runways.py
index 5323c37b..658cc846 100644
--- a/gen/runways.py
+++ b/gen/runways.py
@@ -7,8 +7,8 @@ from typing import Iterator, Optional
from dcs.terrain.terrain import Airport
+from game.theater import ControlPoint, ControlPointType
from game.weather import Conditions
-from theater import ControlPoint, ControlPointType
from .airfields import AIRFIELD_DATA
from .radios import RadioFrequency
from .tacan import TacanChannel
diff --git a/qt_ui/widgets/base/QAirportInformation.py b/qt_ui/widgets/base/QAirportInformation.py
deleted file mode 100644
index 4fc1474c..00000000
--- a/qt_ui/widgets/base/QAirportInformation.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QVBoxLayout, QLCDNumber
-
-from theater import ControlPoint, Airport
-
-
-class QAirportInformation(QGroupBox):
-
- def __init__(self, cp:ControlPoint, airport:Airport):
- super(QAirportInformation, self).__init__(airport.name)
- self.cp = cp
- self.airport = airport
- self.init_ui()
-
- def init_ui(self):
- self.layout = QGridLayout()
-
- # Runway information
- self.runways = QGroupBox("Runways")
- self.runwayLayout = QGridLayout()
- for i, runway in enumerate(self.airport.runways):
-
- # Seems like info is missing in pydcs, even if the attribute is there
- lr = ""
- if runway.leftright == 1:
- lr = "L"
- elif runway.leftright == 2:
- lr = "R"
-
- self.runwayLayout.addWidget(QLabel("Runway " + str(runway.heading) + lr), i, 0)
-
- # Seems like info is missing in pydcs, even if the attribute is there
- if runway.ils:
- self.runwayLayout.addWidget(QLabel("ILS "), i, 1)
- self.runwayLayout.addWidget(QLCDNumber(6, runway.ils), i, 1)
- else:
- self.runwayLayout.addWidget(QLabel("NO ILS"), i, 1)
-
-
- self.runways.setLayout(self.runwayLayout)
- self.layout.addWidget(self.runways, 0, 0)
-
- self.layout.addWidget(QLabel("Parking Slots :"), 1, 0)
- self.layout.addWidget(QLabel(str(len(self.airport.parking_slots))), 1, 1)
-
-
- stretch = QVBoxLayout()
- stretch.addStretch()
-
- self.layout.addLayout(stretch, 2, 0)
- self.setLayout(self.layout)
-
-
diff --git a/qt_ui/widgets/combos/QAircraftTypeSelector.py b/qt_ui/widgets/combos/QAircraftTypeSelector.py
index 1f490e4d..2be6e48c 100644
--- a/qt_ui/widgets/combos/QAircraftTypeSelector.py
+++ b/qt_ui/widgets/combos/QAircraftTypeSelector.py
@@ -3,13 +3,13 @@ from typing import Iterable
from PySide2.QtWidgets import QComboBox
-from dcs.planes import PlaneType
+from dcs.unittype import FlyingType
class QAircraftTypeSelector(QComboBox):
"""Combo box for selecting among the given aircraft types."""
- def __init__(self, aircraft_types: Iterable[PlaneType]) -> None:
+ def __init__(self, aircraft_types: Iterable[FlyingType]) -> None:
super().__init__()
for aircraft in aircraft_types:
self.addItem(f"{aircraft.id}", userData=aircraft)
diff --git a/qt_ui/widgets/combos/QArrivalAirfieldSelector.py b/qt_ui/widgets/combos/QArrivalAirfieldSelector.py
new file mode 100644
index 00000000..c5d89b90
--- /dev/null
+++ b/qt_ui/widgets/combos/QArrivalAirfieldSelector.py
@@ -0,0 +1,40 @@
+"""Combo box for selecting a departure airfield."""
+from typing import Iterable
+
+from PySide2.QtWidgets import QComboBox
+from dcs.unittype import FlyingType
+
+from game import db
+from game.theater.controlpoint import ControlPoint
+
+
+class QArrivalAirfieldSelector(QComboBox):
+ """A combo box for selecting a flight's arrival or divert airfield.
+
+ The combo box will automatically be populated with all airfields the given
+ aircraft type is able to land at.
+ """
+
+ def __init__(self, destinations: Iterable[ControlPoint],
+ aircraft: FlyingType, optional_text: str) -> None:
+ super().__init__()
+ self.destinations = list(destinations)
+ self.aircraft = aircraft
+ self.optional_text = optional_text
+ self.rebuild_selector()
+ self.setCurrentIndex(0)
+
+ def change_aircraft(self, aircraft: FlyingType) -> None:
+ if self.aircraft == aircraft:
+ return
+ self.aircraft = aircraft
+ self.rebuild_selector()
+
+ def rebuild_selector(self) -> None:
+ self.clear()
+ for destination in self.destinations:
+ if destination.can_land(self.aircraft):
+ self.addItem(destination.name, destination)
+ self.model().sort(0)
+ self.insertItem(0, self.optional_text, None)
+ self.update()
diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py
index 6ba9e455..1918dd4d 100644
--- a/qt_ui/widgets/combos/QFlightTypeComboBox.py
+++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py
@@ -2,7 +2,7 @@
from PySide2.QtWidgets import QComboBox
-from theater import ConflictTheater, MissionTarget
+from game.theater import ConflictTheater, MissionTarget
class QFlightTypeComboBox(QComboBox):
diff --git a/qt_ui/widgets/combos/QOriginAirfieldSelector.py b/qt_ui/widgets/combos/QOriginAirfieldSelector.py
index 14bdbb47..ce1c6301 100644
--- a/qt_ui/widgets/combos/QOriginAirfieldSelector.py
+++ b/qt_ui/widgets/combos/QOriginAirfieldSelector.py
@@ -3,7 +3,7 @@ from typing import Iterable
from PySide2.QtCore import Signal
from PySide2.QtWidgets import QComboBox
-from dcs.planes import PlaneType
+from dcs.unittype import FlyingType
from game.inventory import GlobalAircraftInventory
from game.theater.controlpoint import ControlPoint
@@ -20,7 +20,7 @@ class QOriginAirfieldSelector(QComboBox):
def __init__(self, global_inventory: GlobalAircraftInventory,
origins: Iterable[ControlPoint],
- aircraft: PlaneType) -> None:
+ aircraft: FlyingType) -> None:
super().__init__()
self.global_inventory = global_inventory
self.origins = list(origins)
@@ -28,7 +28,7 @@ class QOriginAirfieldSelector(QComboBox):
self.rebuild_selector()
self.currentIndexChanged.connect(self.index_changed)
- def change_aircraft(self, aircraft: PlaneType) -> None:
+ def change_aircraft(self, aircraft: FlyingType) -> None:
if self.aircraft == aircraft:
return
self.aircraft = aircraft
diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
index 8af3c3f4..8f40afde 100644
--- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
+++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
@@ -1,10 +1,10 @@
from PySide2.QtGui import QStandardItem, QStandardItemModel
from game import Game
+from game.theater import ControlPointType
from gen import BuildingGroundObject, Conflict, FlightWaypointType
from gen.flights.flight import FlightWaypoint
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
-from theater import ControlPointType
class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
diff --git a/qt_ui/widgets/map/QFrontLine.py b/qt_ui/widgets/map/QFrontLine.py
index 1849f5ff..2ca71953 100644
--- a/qt_ui/widgets/map/QFrontLine.py
+++ b/qt_ui/widgets/map/QFrontLine.py
@@ -13,11 +13,11 @@ from PySide2.QtWidgets import (
)
import qt_ui.uiconstants as const
+from game.theater import FrontLine
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
-from theater import FrontLine
class QFrontLine(QGraphicsLineItem):
diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py
index fb5802c3..50fc5fba 100644
--- a/qt_ui/widgets/map/QLiberationMap.py
+++ b/qt_ui/widgets/map/QLiberationMap.py
@@ -3,7 +3,7 @@ from __future__ import annotations
import datetime
import logging
import math
-from typing import Iterable, List, Optional, Tuple, Iterator
+from typing import Iterable, Iterator, List, Optional, Tuple
from PySide2.QtCore import QPointF, Qt
from PySide2.QtGui import (
@@ -27,6 +27,13 @@ from dcs.mapping import point_from_heading
import qt_ui.uiconstants as CONST
from game import Game, db
+from game.theater import ControlPoint
+from game.theater.conflicttheater import FrontLine
+from game.theater.theatergroundobject import (
+ EwrGroundObject,
+ MissileSiteGroundObject,
+ TheaterGroundObject,
+)
from game.utils import meter_to_feet
from game.weather import TimeOfDay
from gen import Conflict
@@ -39,13 +46,7 @@ from qt_ui.widgets.map.QLiberationScene import QLiberationScene
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
-from theater import ControlPoint
-from game.theater.conflicttheater import FrontLine
-from game.theater.theatergroundobject import (
- EwrGroundObject,
- MissileSiteGroundObject,
- TheaterGroundObject,
-)
+
def binomial(i: int, n: int) -> float:
"""Binomial coefficient"""
@@ -373,6 +374,10 @@ class QLiberationMap(QGraphicsView):
FlightWaypointType.TARGET_SHIP,
)
for idx, point in enumerate(flight.flight_plan.waypoints[1:]):
+ if point.waypoint_type == FlightWaypointType.DIVERT:
+ # Don't clutter the map showing divert points.
+ continue
+
new_pos = self._transform_point(Point(point.x, point.y))
self.draw_flight_path(scene, prev_pos, new_pos, is_player,
selected)
@@ -386,7 +391,6 @@ class QLiberationMap(QGraphicsView):
self.draw_waypoint_info(scene, idx + 1, point, new_pos,
flight.flight_plan)
prev_pos = tuple(new_pos)
- self.draw_flight_path(scene, prev_pos, pos, is_player, selected)
def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
player: bool, selected: bool) -> None:
diff --git a/qt_ui/widgets/map/QMapControlPoint.py b/qt_ui/widgets/map/QMapControlPoint.py
index e59cdbfb..0f88bf7e 100644
--- a/qt_ui/widgets/map/QMapControlPoint.py
+++ b/qt_ui/widgets/map/QMapControlPoint.py
@@ -4,9 +4,9 @@ from PySide2.QtGui import QColor, QPainter
from PySide2.QtWidgets import QAction, QMenu
import qt_ui.uiconstants as const
+from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
-from theater import ControlPoint
from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions
from ...windows.GameUpdateSignal import GameUpdateSignal
@@ -79,11 +79,8 @@ class QMapControlPoint(QMapObject):
for connected in self.control_point.connected_points:
if connected.captured:
+ menu.addAction(self.capture_action)
break
- else:
- return
-
- menu.addAction(self.capture_action)
def cheat_capture(self) -> None:
self.control_point.capture(self.game_model.game, for_player=True)
diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py
index a7d857f3..f1d3e542 100644
--- a/qt_ui/widgets/map/QMapGroundObject.py
+++ b/qt_ui/widgets/map/QMapGroundObject.py
@@ -8,8 +8,8 @@ import qt_ui.uiconstants as const
from game import Game
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import REWARDS
+from game.theater import ControlPoint, TheaterGroundObject
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
-from theater import ControlPoint, TheaterGroundObject
from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions
diff --git a/qt_ui/widgets/map/QMapObject.py b/qt_ui/widgets/map/QMapObject.py
index a3c57c19..16f07061 100644
--- a/qt_ui/widgets/map/QMapObject.py
+++ b/qt_ui/widgets/map/QMapObject.py
@@ -47,9 +47,12 @@ class QMapObject(QGraphicsRectItem):
object_details_action.triggered.connect(self.on_click)
menu.addAction(object_details_action)
- new_package_action = QAction(f"New package")
- new_package_action.triggered.connect(self.open_new_package_dialog)
- menu.addAction(new_package_action)
+ # Not all locations have valid objetives. Off-map spawns, for example,
+ # have no mission types.
+ if list(self.mission_target.mission_types(for_player=True)):
+ new_package_action = QAction(f"New package")
+ new_package_action.triggered.connect(self.open_new_package_dialog)
+ menu.addAction(new_package_action)
self.add_context_menu_actions(menu)
diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py
index cf5e1a34..3740448a 100644
--- a/qt_ui/windows/basemenu/QBaseMenu2.py
+++ b/qt_ui/windows/basemenu/QBaseMenu2.py
@@ -2,12 +2,12 @@ from PySide2.QtCore import Qt
from PySide2.QtGui import QCloseEvent, QPixmap
from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget
+from game.theater import ControlPoint, ControlPointType
from qt_ui.models import GameModel
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
-from theater import ControlPoint, ControlPointType
class QBaseMenu2(QDialog):
@@ -18,7 +18,6 @@ class QBaseMenu2(QDialog):
# Attrs
self.cp = cp
self.game_model = game_model
- self.is_carrier = self.cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
self.objectName = "menuDialogue"
# Widgets
@@ -58,7 +57,7 @@ class QBaseMenu2(QDialog):
title = QLabel("" + self.cp.name + "")
title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title")
- unitsPower = QLabel("{} / {} / Runway : {}".format(self.cp.base.total_planes, self.cp.base.total_armor,
+ unitsPower = QLabel("{} / {} / Runway : {}".format(self.cp.base.total_aircraft, self.cp.base.total_armor,
"Available" if self.cp.has_runway() else "Unavailable"))
self.topLayout.addWidget(title)
self.topLayout.addWidget(unitsPower)
diff --git a/qt_ui/windows/basemenu/QBaseMenuTabs.py b/qt_ui/windows/basemenu/QBaseMenuTabs.py
index 0c82c86e..1e705372 100644
--- a/qt_ui/windows/basemenu/QBaseMenuTabs.py
+++ b/qt_ui/windows/basemenu/QBaseMenuTabs.py
@@ -1,43 +1,34 @@
-from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QTabWidget
+from PySide2.QtWidgets import QTabWidget
+from game.theater import ControlPoint, OffMapSpawn
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand
from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ
from qt_ui.windows.basemenu.ground_forces.QGroundForcesHQ import QGroundForcesHQ
from qt_ui.windows.basemenu.intel.QIntelInfo import QIntelInfo
-from theater import ControlPoint
class QBaseMenuTabs(QTabWidget):
def __init__(self, cp: ControlPoint, game_model: GameModel):
super(QBaseMenuTabs, self).__init__()
- self.cp = cp
- if cp:
-
- if not cp.captured:
- if not cp.is_carrier:
- self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
- self.addTab(self.base_defenses_hq, "Base Defenses")
- self.intel = QIntelInfo(cp, game_model.game)
- self.addTab(self.intel, "Intel")
- else:
- if cp.has_runway():
- self.airfield_command = QAirfieldCommand(cp, game_model)
- self.addTab(self.airfield_command, "Airfield Command")
-
- if not cp.is_carrier:
- self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
- self.addTab(self.ground_forces_hq, "Ground Forces HQ")
- self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
- self.addTab(self.base_defenses_hq, "Base Defenses")
- else:
- self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
- self.addTab(self.base_defenses_hq, "Fleet")
+ if not cp.captured:
+ if not cp.is_carrier and not isinstance(cp, OffMapSpawn):
+ self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
+ self.addTab(self.base_defenses_hq, "Base Defenses")
+ self.intel = QIntelInfo(cp, game_model.game)
+ self.addTab(self.intel, "Intel")
else:
- tabError = QFrame()
- l = QGridLayout()
- l.addWidget(QLabel("No Control Point"))
- tabError.setLayout(l)
- self.addTab(tabError, "No Control Point")
\ No newline at end of file
+ if cp.has_runway():
+ self.airfield_command = QAirfieldCommand(cp, game_model)
+ self.addTab(self.airfield_command, "Airfield Command")
+
+ if cp.is_carrier:
+ self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
+ self.addTab(self.base_defenses_hq, "Fleet")
+ elif not isinstance(cp, OffMapSpawn):
+ self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
+ self.addTab(self.ground_forces_hq, "Ground Forces HQ")
+ self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
+ self.addTab(self.base_defenses_hq, "Base Defenses")
\ No newline at end of file
diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py
index b41ac68a..7f462c57 100644
--- a/qt_ui/windows/basemenu/QRecruitBehaviour.py
+++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py
@@ -1,3 +1,6 @@
+import logging
+from typing import Type
+
from PySide2.QtWidgets import (
QGroupBox,
QHBoxLayout,
@@ -6,17 +9,17 @@ from PySide2.QtWidgets import (
QSizePolicy,
QSpacerItem,
)
-import logging
from dcs.unittype import UnitType
-from theater import db
-
+from game import db
+from game.event import UnitsDeliveryEvent
+from game.theater import ControlPoint
+from qt_ui.models import GameModel
class QRecruitBehaviour:
- game = None
- cp = None
- deliveryEvent = None
+ game_model: GameModel
+ cp: ControlPoint
existing_units_labels = None
bought_amount_labels = None
maximum_units = -1
@@ -24,12 +27,16 @@ class QRecruitBehaviour:
BUDGET_FORMAT = "Available Budget: ${}M"
def __init__(self) -> None:
- self.deliveryEvent = None
self.bought_amount_labels = {}
self.existing_units_labels = {}
self.recruitable_types = []
self.update_available_budget()
+ @property
+ def pending_deliveries(self) -> UnitsDeliveryEvent:
+ assert self.cp.pending_unit_deliveries
+ return self.cp.pending_unit_deliveries
+
@property
def budget(self) -> int:
return self.game_model.game.budget
@@ -47,7 +54,7 @@ class QRecruitBehaviour:
exist.setLayout(existLayout)
existing_units = self.cp.base.total_units_of_type(unit_type)
- scheduled_units = self.deliveryEvent.units.get(unit_type, 0)
+ scheduled_units = self.pending_deliveries.units.get(unit_type, 0)
unitName = QLabel("" + db.unit_type_name_2(unit_type) + "")
unitName.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
@@ -100,10 +107,10 @@ class QRecruitBehaviour:
return row + 1
- def _update_count_label(self, unit_type: UnitType):
+ def _update_count_label(self, unit_type: Type[UnitType]):
self.bought_amount_labels[unit_type].setText("{}".format(
- unit_type in self.deliveryEvent.units and "{}".format(self.deliveryEvent.units[unit_type]) or "0"
+ unit_type in self.pending_deliveries.units and "{}".format(self.pending_deliveries.units[unit_type]) or "0"
))
self.existing_units_labels[unit_type].setText("{}".format(
@@ -119,17 +126,10 @@ class QRecruitBehaviour:
child.setText(
QRecruitBehaviour.BUDGET_FORMAT.format(self.budget))
- def buy(self, unit_type):
-
- if self.maximum_units > 0:
- if self.total_units + 1 > self.maximum_units:
- logging.info("Not enough space left !")
- # TODO : display modal warning
- return
-
+ def buy(self, unit_type: Type[UnitType]):
price = db.PRICES[unit_type]
if self.budget >= price:
- self.deliveryEvent.deliver({unit_type: 1})
+ self.pending_deliveries.deliver({unit_type: 1})
self.budget -= price
else:
# TODO : display modal warning
@@ -138,12 +138,12 @@ class QRecruitBehaviour:
self.update_available_budget()
def sell(self, unit_type):
- if self.deliveryEvent.units.get(unit_type, 0) > 0:
+ if self.pending_deliveries.units.get(unit_type, 0) > 0:
price = db.PRICES[unit_type]
self.budget += price
- self.deliveryEvent.units[unit_type] = self.deliveryEvent.units[unit_type] - 1
- if self.deliveryEvent.units[unit_type] == 0:
- del self.deliveryEvent.units[unit_type]
+ self.pending_deliveries.units[unit_type] = self.pending_deliveries.units[unit_type] - 1
+ if self.pending_deliveries.units[unit_type] == 0:
+ del self.pending_deliveries.units[unit_type]
elif self.cp.base.total_units_of_type(unit_type) > 0:
price = db.PRICES[unit_type]
self.budget += price
@@ -152,25 +152,6 @@ class QRecruitBehaviour:
self._update_count_label(unit_type)
self.update_available_budget()
- @property
- def total_units(self):
-
- total = 0
- for unit_type in self.recruitables_types:
- total += self.cp.base.total_units(unit_type)
- print(unit_type, total, self.cp.base.total_units(unit_type))
- print("--------------------------------")
-
- if self.deliveryEvent:
- for unit_bought in self.deliveryEvent.units:
- if db.unit_task(unit_bought) in self.recruitables_types:
- total += self.deliveryEvent.units[unit_bought]
- print(unit_bought, total, self.deliveryEvent.units[unit_bought])
-
- print("=============================")
-
- return total
-
def set_maximum_units(self, maximum_units):
"""
Set the maximum number of units that can be bought
@@ -181,4 +162,4 @@ class QRecruitBehaviour:
"""
Set the maximum number of units that can be bought
"""
- self.recruitables_types = recruitables_types
\ No newline at end of file
+ self.recruitables_types = recruitables_types
diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
index a01aaaa9..7c159e94 100644
--- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
+++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
@@ -1,3 +1,4 @@
+import logging
from typing import Optional, Set
from PySide2.QtCore import Qt
@@ -11,13 +12,14 @@ from PySide2.QtWidgets import (
QVBoxLayout,
QWidget,
)
+from dcs.task import CAP, CAS
from dcs.unittype import UnitType
-from game.event.event import UnitsDeliveryEvent
+from game import db
+from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.uiconstants import ICONS
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
-from theater import CAP, CAS, ControlPoint, db
class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
@@ -25,25 +27,18 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
QFrame.__init__(self)
self.cp = cp
self.game_model = game_model
- self.deliveryEvent: Optional[UnitsDeliveryEvent] = None
self.bought_amount_labels = {}
self.existing_units_labels = {}
- for event in self.game_model.game.events:
- if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp:
- self.deliveryEvent = event
- if not self.deliveryEvent:
- self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp)
-
# Determine maximum number of aircrafts that can be bought
- self.set_maximum_units(self.cp.available_aircraft_slots)
+ self.set_maximum_units(self.cp.total_aircraft_parking)
self.set_recruitable_types([CAP, CAS])
self.bought_amount_labels = {}
self.existing_units_labels = {}
- self.hangar_status = QHangarStatus(self.total_units, self.cp.available_aircraft_slots)
+ self.hangar_status = QHangarStatus(self.cp)
self.init_ui()
@@ -86,13 +81,18 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
self.setLayout(main_layout)
def buy(self, unit_type):
+ if self.maximum_units > 0:
+ if self.cp.unclaimed_parking <= 0:
+ logging.debug(f"No space for additional aircraft at {self.cp}.")
+ return
+
super().buy(unit_type)
- self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
+ self.hangar_status.update_label()
def sell(self, unit_type: UnitType):
# Don't need to remove aircraft from the inventory if we're canceling
# orders.
- if self.deliveryEvent.units.get(unit_type, 0) <= 0:
+ if self.pending_deliveries.units.get(unit_type, 0) <= 0:
global_inventory = self.game_model.game.aircraft_inventory
inventory = global_inventory.for_control_point(self.cp)
try:
@@ -105,22 +105,26 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
"assigned to a mission?", QMessageBox.Ok)
return
super().sell(unit_type)
- self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
+ self.hangar_status.update_label()
class QHangarStatus(QHBoxLayout):
- def __init__(self, current_amount: int, max_amount: int):
- super(QHangarStatus, self).__init__()
+ def __init__(self, control_point: ControlPoint) -> None:
+ super().__init__()
+ self.control_point = control_point
+
self.icon = QLabel()
self.icon.setPixmap(ICONS["Hangar"])
self.text = QLabel("")
- self.update_label(current_amount, max_amount)
+ self.update_label()
self.addWidget(self.icon, Qt.AlignLeft)
self.addWidget(self.text, Qt.AlignLeft)
self.addStretch(50)
self.setAlignment(Qt.AlignLeft)
- def update_label(self, current_amount: int, max_amount: int):
- self.text.setText("{}/{}".format(current_amount, max_amount))
+ def update_label(self) -> None:
+ current_amount = self.control_point.expected_aircraft_next_turn
+ max_amount = self.control_point.total_aircraft_parking
+ self.text.setText(f"{current_amount}/{max_amount}")
diff --git a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py
index 9965115a..97c804bf 100644
--- a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py
+++ b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py
@@ -1,10 +1,10 @@
from PySide2.QtWidgets import QFrame, QGridLayout, QGroupBox, QVBoxLayout
+from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import \
QAircraftRecruitmentMenu
from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView
-from theater import ControlPoint
class QAirfieldCommand(QFrame):
diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py
index 350cf5e8..6d46b35b 100644
--- a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py
+++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py
@@ -1,10 +1,16 @@
from PySide2.QtCore import Qt
-from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout
+from PySide2.QtWidgets import (
+ QGridLayout,
+ QGroupBox,
+ QLabel,
+ QPushButton,
+ QVBoxLayout,
+)
+from game.theater import ControlPoint, TheaterGroundObject
from qt_ui.dialogs import Dialog
from qt_ui.uiconstants import VEHICLES_ICONS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
-from theater import ControlPoint, TheaterGroundObject
class QBaseDefenseGroupInfo(QGroupBox):
diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py
index 5ad1f6c9..75a45eb0 100644
--- a/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py
+++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py
@@ -1,7 +1,9 @@
from PySide2.QtWidgets import QFrame, QGridLayout
+
from game import Game
-from qt_ui.windows.basemenu.base_defenses.QBaseInformation import QBaseInformation
-from theater import ControlPoint
+from game.theater import ControlPoint
+from qt_ui.windows.basemenu.base_defenses.QBaseInformation import \
+ QBaseInformation
class QBaseDefensesHQ(QFrame):
diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py b/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py
index f5325887..50ec2f81 100644
--- a/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py
+++ b/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py
@@ -1,10 +1,15 @@
from PySide2.QtGui import Qt
-from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QVBoxLayout, QFrame, QWidget, QScrollArea
+from PySide2.QtWidgets import (
+ QFrame,
+ QGridLayout,
+ QScrollArea,
+ QVBoxLayout,
+ QWidget,
+)
-from game import db
-from qt_ui.uiconstants import AIRCRAFT_ICONS, VEHICLES_ICONS
-from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import QBaseDefenseGroupInfo
-from theater import ControlPoint, Airport
+from game.theater import Airport, ControlPoint
+from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import \
+ QBaseDefenseGroupInfo
class QBaseInformation(QFrame):
diff --git a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py
index ec1cabf6..c359eaaf 100644
--- a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py
+++ b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py
@@ -6,11 +6,12 @@ from PySide2.QtWidgets import (
QVBoxLayout,
QWidget,
)
+from dcs.task import PinpointStrike
-from game.event import UnitsDeliveryEvent
+from game import db
+from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
-from theater import ControlPoint, PinpointStrike, db
class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
@@ -23,12 +24,6 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
self.bought_amount_labels = {}
self.existing_units_labels = {}
- for event in self.game_model.game.events:
- if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp:
- self.deliveryEvent = event
- if not self.deliveryEvent:
- self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp)
-
self.init_ui()
def init_ui(self):
@@ -61,4 +56,4 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
scroll.setWidgetResizable(True)
scroll.setWidget(scroll_content)
main_layout.addWidget(scroll)
- self.setLayout(main_layout)
\ No newline at end of file
+ self.setLayout(main_layout)
diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py
index bb18594f..39cba843 100644
--- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py
+++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py
@@ -1,11 +1,11 @@
from PySide2.QtWidgets import QFrame, QGridLayout
+from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import \
QArmorRecruitmentMenu
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import \
QGroundForcesStrategy
-from theater import ControlPoint
class QGroundForcesHQ(QFrame):
diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py
index 0b7b4db6..3aee8c50 100644
--- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py
+++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py
@@ -1,8 +1,9 @@
-from PySide2.QtWidgets import QLabel, QGroupBox, QVBoxLayout
+from PySide2.QtWidgets import QGroupBox, QLabel, QVBoxLayout
from game import Game
-from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import QGroundForcesStrategySelector
-from theater import ControlPoint
+from game.theater import ControlPoint
+from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import \
+ QGroundForcesStrategySelector
class QGroundForcesStrategy(QGroupBox):
diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py
index 09c3fa5b..4acd8731 100644
--- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py
+++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py
@@ -1,6 +1,6 @@
from PySide2.QtWidgets import QComboBox
-from theater import ControlPoint, CombatStance
+from game.theater import CombatStance, ControlPoint
class QGroundForcesStrategySelector(QComboBox):
diff --git a/qt_ui/windows/basemenu/intel/QIntelInfo.py b/qt_ui/windows/basemenu/intel/QIntelInfo.py
index bc7cb13b..e422ef3a 100644
--- a/qt_ui/windows/basemenu/intel/QIntelInfo.py
+++ b/qt_ui/windows/basemenu/intel/QIntelInfo.py
@@ -1,11 +1,14 @@
+from PySide2.QtWidgets import (
+ QFrame,
+ QGridLayout,
+ QGroupBox,
+ QLabel,
+ QVBoxLayout,
+)
+from dcs.task import CAP, CAS, Embarking, PinpointStrike
-
-from PySide2.QtWidgets import QLabel, QGroupBox, QVBoxLayout, QFrame, QGridLayout
-from dcs.task import Embarking, CAS, PinpointStrike, CAP
-
-from game import Game
-from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import QGroundForcesStrategySelector
-from theater import ControlPoint, db
+from game import Game, db
+from game.theater import ControlPoint
class QIntelInfo(QFrame):
diff --git a/qt_ui/windows/groundobject/QBuildingInfo.py b/qt_ui/windows/groundobject/QBuildingInfo.py
index e474a59f..fcf6366b 100644
--- a/qt_ui/windows/groundobject/QBuildingInfo.py
+++ b/qt_ui/windows/groundobject/QBuildingInfo.py
@@ -2,7 +2,7 @@ import os
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QVBoxLayout, QLabel
-
+from game.db import REWARDS
class QBuildingInfo(QGroupBox):
@@ -28,6 +28,13 @@ class QBuildingInfo(QGroupBox):
layout = QVBoxLayout()
layout.addWidget(self.header)
layout.addWidget(self.name)
+
+ if self.building.category in REWARDS.keys():
+ income_label_text = 'Value: ' + str(REWARDS[self.building.category]) + "M"
+ if self.building.is_dead:
+ income_label_text = '' + income_label_text + ''
+ self.reward = QLabel(income_label_text)
+ layout.addWidget(self.reward)
+
footer = QHBoxLayout()
self.setLayout(layout)
-
diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py
index dcfed0a3..abbf5c8c 100644
--- a/qt_ui/windows/groundobject/QGroundObjectMenu.py
+++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py
@@ -2,20 +2,31 @@ import logging
from PySide2 import QtCore
from PySide2.QtGui import Qt
-from PySide2.QtWidgets import QHBoxLayout, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton, \
- QComboBox, QSpinBox, QMessageBox
+from PySide2.QtWidgets import (
+ QComboBox,
+ QDialog,
+ QGridLayout,
+ QGroupBox,
+ QHBoxLayout,
+ QLabel,
+ QMessageBox,
+ QPushButton,
+ QSpinBox,
+ QVBoxLayout,
+)
from dcs import Point
from game import Game, db
from game.data.building_data import FORTIFICATION_BUILDINGS
-from game.db import PRICES, unit_type_of, PinpointStrike
-from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size
+from game.db import PRICES, PinpointStrike, REWARDS, unit_type_of
+from game.theater import ControlPoint, TheaterGroundObject
+from gen.defenses.armor_group_generator import \
+ generate_armor_group_of_type_and_size
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo
-from theater import ControlPoint, TheaterGroundObject
class QGroundObjectMenu(QDialog):
@@ -51,6 +62,8 @@ class QGroundObjectMenu(QDialog):
self.mainLayout.addWidget(self.intelBox)
else:
self.mainLayout.addWidget(self.buildingBox)
+ if self.cp.captured:
+ self.mainLayout.addWidget(self.financesBox)
self.actionLayout = QHBoxLayout()
@@ -104,12 +117,26 @@ class QGroundObjectMenu(QDialog):
self.buildingBox = QGroupBox("Buildings :")
self.buildingsLayout = QGridLayout()
+
j = 0
+ total_income = 0
+ received_income = 0
for i, building in enumerate(self.buildings):
if building.dcs_identifier not in FORTIFICATION_BUILDINGS:
self.buildingsLayout.addWidget(QBuildingInfo(building, self.ground_object), j/3, j%3)
j = j + 1
+ if building.category in REWARDS.keys():
+ total_income = total_income + REWARDS[building.category]
+ if not building.is_dead:
+ received_income = received_income + REWARDS[building.category]
+
+ self.financesBox = QGroupBox("Finances: ")
+ self.financesBoxLayout = QGridLayout()
+ self.financesBoxLayout.addWidget(QLabel("Available: " + str(total_income) + "M"), 2, 1)
+ self.financesBoxLayout.addWidget(QLabel("Receiving: " + str(received_income) + "M"), 2, 2)
+
+ self.financesBox.setLayout(self.financesBoxLayout)
self.buildingBox.setLayout(self.buildingsLayout)
self.intelBox.setLayout(self.intelLayout)
diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py
index f4fe6041..0e0bf773 100644
--- a/qt_ui/windows/mission/flight/QFlightCreator.py
+++ b/qt_ui/windows/mission/flight/QFlightCreator.py
@@ -10,15 +10,17 @@ from PySide2.QtWidgets import (
from dcs.planes import PlaneType
from game import Game
+from game.theater import ControlPoint, OffMapSpawn
from gen.ato import Package
from gen.flights.flight import Flight
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector
+from qt_ui.widgets.combos.QArrivalAirfieldSelector import \
+ QArrivalAirfieldSelector
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
-from theater import ControlPoint
class QFlightCreator(QDialog):
@@ -49,16 +51,30 @@ class QFlightCreator(QDialog):
self.on_aircraft_changed)
layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector))
- self.airfield_selector = QOriginAirfieldSelector(
+ self.departure = QOriginAirfieldSelector(
self.game.aircraft_inventory,
[cp for cp in game.theater.controlpoints if cp.captured],
self.aircraft_selector.currentData()
)
- self.airfield_selector.availability_changed.connect(self.update_max_size)
- layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector))
+ self.departure.availability_changed.connect(self.update_max_size)
+ layout.addLayout(QLabeledWidget("Departure:", self.departure))
+
+ self.arrival = QArrivalAirfieldSelector(
+ [cp for cp in game.theater.controlpoints if cp.captured],
+ self.aircraft_selector.currentData(),
+ "Same as departure"
+ )
+ layout.addLayout(QLabeledWidget("Arrival:", self.arrival))
+
+ self.divert = QArrivalAirfieldSelector(
+ [cp for cp in game.theater.controlpoints if cp.captured],
+ self.aircraft_selector.currentData(),
+ "None"
+ )
+ layout.addLayout(QLabeledWidget("Divert:", self.divert))
self.flight_size_spinner = QFlightSizeSpinner()
- self.update_max_size(self.airfield_selector.available)
+ self.update_max_size(self.departure.available)
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
self.client_slots_spinner = QFlightSizeSpinner(
@@ -82,10 +98,16 @@ class QFlightCreator(QDialog):
def verify_form(self) -> Optional[str]:
aircraft: PlaneType = self.aircraft_selector.currentData()
- origin: ControlPoint = self.airfield_selector.currentData()
+ origin: ControlPoint = self.departure.currentData()
+ arrival: ControlPoint = self.arrival.currentData()
+ divert: ControlPoint = self.divert.currentData()
size: int = self.flight_size_spinner.value()
if not origin.captured:
return f"{origin.name} is not owned by your coalition."
+ if arrival is not None and not arrival.captured:
+ return f"{arrival.name} is not owned by your coalition."
+ if divert is not None and not divert.captured:
+ return f"{divert.name} is not owned by your coalition."
available = origin.base.aircraft.get(aircraft, 0)
if not available:
return f"{origin.name} has no {aircraft.id} available."
@@ -104,14 +126,22 @@ class QFlightCreator(QDialog):
task = self.task_selector.currentData()
aircraft = self.aircraft_selector.currentData()
- origin = self.airfield_selector.currentData()
+ origin = self.departure.currentData()
+ arrival = self.arrival.currentData()
+ divert = self.divert.currentData()
size = self.flight_size_spinner.value()
- if self.game.settings.perf_ai_parking_start:
+ if arrival is None:
+ arrival = origin
+
+ if isinstance(origin, OffMapSpawn):
+ start_type = "In Flight"
+ elif self.game.settings.perf_ai_parking_start:
start_type = "Cold"
else:
start_type = "Warm"
- flight = Flight(self.package, aircraft, size, origin, task, start_type)
+ flight = Flight(self.package, aircraft, size, task, start_type, origin,
+ arrival, divert)
flight.client_count = self.client_slots_spinner.value()
# noinspection PyUnresolvedReferences
@@ -120,7 +150,9 @@ class QFlightCreator(QDialog):
def on_aircraft_changed(self, index: int) -> None:
new_aircraft = self.aircraft_selector.itemData(index)
- self.airfield_selector.change_aircraft(new_aircraft)
+ self.departure.change_aircraft(new_aircraft)
+ self.arrival.change_aircraft(new_aircraft)
+ self.divert.change_aircraft(new_aircraft)
def update_max_size(self, available: int) -> None:
self.flight_size_spinner.setMaximum(min(available, 4))
diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py
index 381d8e39..c8d4562f 100644
--- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py
+++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py
@@ -42,15 +42,7 @@ class QFlightWaypointList(QTableView):
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
- # The first waypoint is set up by pydcs at mission generation time, so
- # we need to add that waypoint manually.
- takeoff = FlightWaypoint(self.flight.from_cp.position.x,
- self.flight.from_cp.position.y, 0)
- takeoff.description = "Take Off"
- takeoff.name = takeoff.pretty_name = "Take Off from " + self.flight.from_cp.name
- takeoff.alt_type = "RADIO"
-
- waypoints = itertools.chain([takeoff], self.flight.points)
+ waypoints = self.flight.flight_plan.waypoints
for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, self.flight, waypoint)
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),
diff --git a/qt_ui/windows/newgame/QCampaignList.py b/qt_ui/windows/newgame/QCampaignList.py
index 617869bc..86ce0461 100644
--- a/qt_ui/windows/newgame/QCampaignList.py
+++ b/qt_ui/windows/newgame/QCampaignList.py
@@ -12,7 +12,7 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QAbstractItemView, QListView
import qt_ui.uiconstants as CONST
-from theater import ConflictTheater
+from game.theater import ConflictTheater
@dataclass(frozen=True)
@@ -29,14 +29,16 @@ class Campaign:
data = json.load(campaign_file)
sanitized_theater = data["theater"].replace(" ", "")
- return cls(data["name"], f"Terrain_{sanitized_theater}", data.get("authors", "???"),
- data.get("description", ""), ConflictTheater.from_json(data))
+ return cls(data["name"], f"Terrain_{sanitized_theater}",
+ data.get("authors", "???"),
+ data.get("description", ""),
+ ConflictTheater.from_json(path.parent, data))
def load_campaigns() -> List[Campaign]:
campaign_dir = Path("resources\\campaigns")
campaigns = []
- for path in campaign_dir.iterdir():
+ for path in campaign_dir.glob("*.json"):
try:
logging.debug(f"Loading campaign from {path}...")
campaign = Campaign.from_json(path)
diff --git a/resources/campaigns/inherent_resolve.json b/resources/campaigns/inherent_resolve.json
index fc5969a5..66befcd5 100644
--- a/resources/campaigns/inherent_resolve.json
+++ b/resources/campaigns/inherent_resolve.json
@@ -3,82 +3,5 @@
"theater": "Syria",
"authors": "Khopa",
"description": "
In this scenario, you start from Jordan, and have to fight your way through eastern Syria.
", - "player_points": [ - { - "type": "airbase", - "id": "King Hussein Air College", - "size": 1000, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Incirlik", - "size": 1000, - "importance": 1.4, - "captured_invert": true - }, - { - "type": "carrier", - "id": 1001, - "x": -210000, - "y": -200000, - "captured_invert": true - }, - { - "type": "lha", - "id": 1002, - "x": -131000, - "y": -161000, - "captured_invert": true - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Khalkhalah", - "size": 1000, - "importance": 1.2 - }, - { - "type": "airbase", - "id": "Palmyra", - "size": 1000, - "importance": 1 - }, - { - "type": "airbase", - "id": "Tabqa", - "size": 1000, - "importance": 1 - }, - { - "type": "airbase", - "id": "Jirah", - "size": 1000, - "importance": 1, - "captured_invert": true - } - ], - "links": [ - [ - "Khalkhalah", - "King Hussein Air College" - ], - [ - "Incirlik", - "Incirlik" - ], - [ - "Khalkhalah", - "Palmyra" - ], - [ - "Palmyra", - "Tabqa" - ], - [ - "Jirah", - "Tabqa" - ] - ] + "miz": "inherent_resolve.miz" } \ No newline at end of file diff --git a/resources/campaigns/inherent_resolve.miz b/resources/campaigns/inherent_resolve.miz new file mode 100644 index 00000000..cb4e2f87 Binary files /dev/null and b/resources/campaigns/inherent_resolve.miz differ diff --git a/theater/__init__.py b/theater/__init__.py deleted file mode 100644 index f6b256d8..00000000 --- a/theater/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# For save game compatibility. Remove before 2.3. -from game.theater import * diff --git a/theater/base.py b/theater/base.py deleted file mode 100644 index fc28c91b..00000000 --- a/theater/base.py +++ /dev/null @@ -1,2 +0,0 @@ -# For save compat. Remove in 2.3. -from game.theater.base import * diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py deleted file mode 100644 index e1566178..00000000 --- a/theater/conflicttheater.py +++ /dev/null @@ -1,2 +0,0 @@ -# For save compat. Remove in 2.3. -from game.theater.conflicttheater import * diff --git a/theater/controlpoint.py b/theater/controlpoint.py deleted file mode 100644 index 90a6b164..00000000 --- a/theater/controlpoint.py +++ /dev/null @@ -1,2 +0,0 @@ -# For save compat. Remove in 2.3. -from game.theater.controlpoint import * diff --git a/theater/frontline.py b/theater/frontline.py deleted file mode 100644 index 5ddb5706..00000000 --- a/theater/frontline.py +++ /dev/null @@ -1,3 +0,0 @@ -# For save compat. Remove in 2.3. -from game.theater.frontline import * -from game.theater.conflicttheater import FrontLine \ No newline at end of file diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py deleted file mode 100644 index 3c77455d..00000000 --- a/theater/theatergroundobject.py +++ /dev/null @@ -1,2 +0,0 @@ -# For save compat. Remove in 2.3. -from game.theater.theatergroundobject import *