Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop

This commit is contained in:
Khopa 2020-11-21 14:46:01 +01:00
commit 799b0fae94
65 changed files with 1292 additions and 738 deletions

View File

@ -1244,7 +1244,7 @@ def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id 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: if name in vehicle_map:
return vehicle_map[name] return vehicle_map[name]
elif name in plane_map: elif name in plane_map:

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import logging import logging
import math 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.mapping import Point
from dcs.task import Task from dcs.task import Task
@ -11,12 +11,12 @@ from dcs.unittype import UnitType
from game import db, persistency from game import db, persistency
from game.debriefing import Debriefing from game.debriefing import Debriefing
from game.infos.information import Information 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 gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
if TYPE_CHECKING: if TYPE_CHECKING:
from ..game import Game from ..game import Game
from game.operation.operation import Operation
DIFFICULTY_LOG_BASE = 1.1 DIFFICULTY_LOG_BASE = 1.1
EVENT_DEPARTURE_MAX_DISTANCE = 340000 EVENT_DEPARTURE_MAX_DISTANCE = 340000
@ -107,14 +107,16 @@ class Event:
for destroyed_aircraft in debriefing.killed_aircrafts: for destroyed_aircraft in debriefing.killed_aircrafts:
try: try:
cpid = int(destroyed_aircraft.split("|")[3]) cpid = int(destroyed_aircraft.split("|")[3])
type = db.unit_type_from_name(destroyed_aircraft.split("|")[4]) aircraft = db.unit_type_from_name(
if cpid in cp_map.keys(): destroyed_aircraft.split("|")[4])
if cpid in cp_map:
cp = cp_map[cpid] cp = cp_map[cpid]
if type in cp.base.aircraft.keys(): if aircraft in cp.base.aircraft:
logging.info("Aircraft destroyed : " + str(type)) logging.info(f"Aircraft destroyed: {aircraft}")
cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1) cp.base.aircraft[aircraft] = max(
except Exception as e: 0, cp.base.aircraft[aircraft] - 1)
print(e) except Exception:
logging.exception("Failed to commit destroyed aircraft")
# ------------------------------ # ------------------------------
# Destroyed ground units # Destroyed ground units
@ -123,13 +125,13 @@ class Event:
for killed_ground_unit in debriefing.killed_ground_units: for killed_ground_unit in debriefing.killed_ground_units:
try: try:
cpid = int(killed_ground_unit.split("|")[3]) 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(): if cpid in cp_map.keys():
killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1 killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1
cp = cp_map[cpid] cp = cp_map[cpid]
if type in cp.base.armor.keys(): if aircraft in cp.base.armor.keys():
logging.info("Ground unit destroyed : " + str(type)) logging.info("Ground unit destroyed : " + str(aircraft))
cp.base.armor[type] = max(0, cp.base.armor[type] - 1) cp.base.armor[aircraft] = max(0, cp.base.armor[aircraft] - 1)
except Exception as e: except Exception as e:
print(e) print(e)
@ -352,11 +354,13 @@ class Event:
logging.info(info.text) logging.info(info.text)
class UnitsDeliveryEvent(Event): class UnitsDeliveryEvent(Event):
informational = True 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, super(UnitsDeliveryEvent, self).__init__(game=game,
location=to_cp.position, location=to_cp.position,
from_cp=from_cp, from_cp=from_cp,
@ -364,17 +368,16 @@ class UnitsDeliveryEvent(Event):
attacker_name=attacker_name, attacker_name=attacker_name,
defender_name=defender_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) 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(): for k, v in units.items():
self.units[k] = self.units.get(k, 0) + v self.units[k] = self.units.get(k, 0) + v
def skip(self): def skip(self) -> None:
for k, v in self.units.items(): 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) info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn)
self.game.informations.append(info) self.game.informations.append(info)

View File

@ -26,7 +26,7 @@ from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction from .factions.faction import Faction
from .infos.information import Information from .infos.information import Information
from .settings import Settings from .settings import Settings
from .theater import ConflictTheater, ControlPoint from .theater import ConflictTheater, ControlPoint, OffMapSpawn
from .weather import Conditions, TimeOfDay from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4 COMMISION_UNIT_VARIETY = 4
@ -151,7 +151,7 @@ class Game:
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points()) reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
for cp in self.theater.player_points(): for cp in self.theater.player_points():
for g in cp.ground_objects: 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] reward = reward + REWARDS[g.category]
return reward return reward
else: else:
@ -160,9 +160,6 @@ class Game:
def _budget_player(self): def _budget_player(self):
self.budget += self.budget_reward_amount 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: def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
event = UnitsDeliveryEvent(attacker_name=self.player_name, event = UnitsDeliveryEvent(attacker_name=self.player_name,
defender_name=self.player_name, defender_name=self.player_name,
@ -172,10 +169,6 @@ class Game:
self.events.append(event) self.events.append(event)
return 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): def initiate_event(self, event: Event):
#assert event in self.events #assert event in self.events
logging.info("Generating {} (regular)".format(event)) logging.info("Generating {} (regular)".format(event))
@ -202,12 +195,6 @@ class Game:
LuaPluginManager.load_settings(self.settings) LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater) 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: def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn") logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
@ -248,6 +235,7 @@ class Game:
self.aircraft_inventory.reset() self.aircraft_inventory.reset()
for cp in self.theater.controlpoints: for cp in self.theater.controlpoints:
cp.pending_unit_deliveries = self.units_delivery_event(cp)
self.aircraft_inventory.set_from_control_point(cp) self.aircraft_inventory.set_from_control_point(cp)
# Plan flights & combat for next turn # Plan flights & combat for next turn
@ -274,7 +262,7 @@ class Game:
production = 0.0 production = 0.0
for enemy_point in self.theater.enemy_points(): for enemy_point in self.theater.enemy_points():
for g in enemy_point.ground_objects: 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 + REWARDS[g.category]
production = production * 0.75 production = production * 0.75
@ -289,6 +277,9 @@ class Game:
if len(potential_cp_armor) == 0: if len(potential_cp_armor) == 0:
potential_cp_armor = self.theater.enemy_points() potential_cp_armor = self.theater.enemy_points()
potential_cp_armor = [p for p in potential_cp_armor if
not isinstance(p, OffMapSpawn)]
i = 0 i = 0
potential_units = db.FACTIONS[self.enemy_name].frontline_units potential_units = db.FACTIONS[self.enemy_name].frontline_units
@ -325,7 +316,7 @@ class Game:
if i > 50 or budget_for_aircraft <= 0: if i > 50 or budget_for_aircraft <= 0:
break break
target_cp = random.choice(potential_cp_armor) target_cp = random.choice(potential_cp_armor)
if target_cp.base.total_planes >= MAX_AIRCRAFT: if target_cp.base.total_aircraft >= MAX_AIRCRAFT:
continue continue
unit = random.choice(potential_units) unit = random.choice(potential_units)
price = db.PRICES[unit] * 2 price = db.PRICES[unit] * 2

View File

@ -1,11 +1,15 @@
"""Inventory management APIs.""" """Inventory management APIs."""
from collections import defaultdict from __future__ import annotations
from typing import Dict, Iterable, Iterator, Set, Tuple
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 gen.flights.flight import Flight
from theater import ControlPoint
if TYPE_CHECKING:
from game.theater import ControlPoint
class ControlPointAircraftInventory: class ControlPointAircraftInventory:
@ -13,9 +17,9 @@ class ControlPointAircraftInventory:
def __init__(self, control_point: ControlPoint) -> None: def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point 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. """Adds aircraft to the inventory.
Args: Args:
@ -24,7 +28,7 @@ class ControlPointAircraftInventory:
""" """
self.inventory[aircraft] += count 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. """Removes aircraft from the inventory.
Args: Args:
@ -43,7 +47,7 @@ class ControlPointAircraftInventory:
) )
self.inventory[aircraft] -= count 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. """Returns the number of available aircraft of the given type.
Args: Args:
@ -55,14 +59,14 @@ class ControlPointAircraftInventory:
return 0 return 0
@property @property
def types_available(self) -> Iterator[UnitType]: def types_available(self) -> Iterator[FlyingType]:
"""Iterates over all available aircraft types.""" """Iterates over all available aircraft types."""
for aircraft, count in self.inventory.items(): for aircraft, count in self.inventory.items():
if count > 0: if count > 0:
yield aircraft yield aircraft
@property @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.""" """Iterates over all available aircraft types, including amounts."""
for aircraft, count in self.inventory.items(): for aircraft, count in self.inventory.items():
if count > 0: if count > 0:
@ -102,9 +106,9 @@ class GlobalAircraftInventory:
return self.inventories[control_point] return self.inventories[control_point]
@property @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.""" """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(): for control_point, inventory in self.inventories.items():
if control_point.captured: if control_point.captured:
for aircraft in inventory.types_available: for aircraft in inventory.types_available:

View File

@ -1,4 +1,4 @@
from theater import ControlPoint from game.theater import ControlPoint
class FrontlineData: class FrontlineData:

View File

@ -15,6 +15,7 @@ from dcs.triggers import TriggerStart
from dcs.unittype import UnitType from dcs.unittype import UnitType
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.theater import ControlPoint
from gen import Conflict, FlightType, VisualGenerator from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA from gen.airfields import AIRFIELD_DATA
@ -29,7 +30,6 @@ from gen.kneeboard import KneeboardGenerator
from gen.radios import RadioFrequency, RadioRegistry from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry from gen.tacan import TacanRegistry
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from theater import ControlPoint
from .. import db from .. import db
from ..debriefing import Debriefing from ..debriefing import Debriefing

View File

@ -6,10 +6,10 @@ from typing import Dict, Optional
class Settings: class Settings:
# Generator settings # Generator settings
inverted: bool = False inverted: bool = False
do_not_generate_carrier: bool = False # TODO : implement do_not_generate_carrier: bool = False
do_not_generate_lha: bool = False # TODO : implement do_not_generate_lha: bool = False
do_not_generate_player_navy: bool = True # TODO : implement do_not_generate_player_navy: bool = False
do_not_generate_enemy_navy: bool = True # TODO : implement do_not_generate_enemy_navy: bool = False
# Difficulty settings # Difficulty settings
player_skill: str = "Good" player_skill: str = "Good"

View File

@ -4,9 +4,8 @@ import math
import typing import typing
from typing import Dict, Type from typing import Dict, Type
from dcs.planes import PlaneType
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task 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 dcs.vehicles import AirDefence, Armor
from game import db from game import db
@ -21,20 +20,16 @@ BASE_MIN_STRENGTH = 0
class Base: 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): def __init__(self):
self.aircraft = {} self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor = {} self.armor: Dict[VehicleType, int] = {}
self.aa = {} self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {} self.commision_points: Dict[Type, float] = {}
self.strength = 1 self.strength = 1
@property @property
def total_planes(self) -> int: def total_aircraft(self) -> int:
return sum(self.aircraft.values()) return sum(self.aircraft.values())
@property @property
@ -83,7 +78,7 @@ class Base:
logging.info("{} for {} ({}): {}".format(self, for_type, count, result)) logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
return 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) return self._find_best_unit(self.aircraft, for_type, count)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]: def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
@ -155,7 +150,7 @@ class Base:
if task: if task:
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task]) count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
else: else:
count = self.total_planes count = self.total_aircraft
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength)) 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) 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. # previous logic removed because we always want the full air defense capabilities.
return self.total_aa 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)) return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def scramble_last_defense(self): def scramble_last_defense(self):
# return as many CAP-capable aircraft as we can since this is the last defense of the base # 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) # (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)) 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)) return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def assemble_attack(self) -> typing.Dict[Armor, int]: def assemble_attack(self) -> typing.Dict[Armor, int]:

View File

@ -1,13 +1,28 @@
from __future__ import annotations from __future__ import annotations
import logging import itertools
import json import json
import logging
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property
from itertools import tee from itertools import tee
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union 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.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 ( from dcs.terrain import (
caucasus, caucasus,
nevada, nevada,
@ -16,11 +31,20 @@ from dcs.terrain import (
syria, syria,
thechannel, 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 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 .landmap import Landmap, load_landmap, poly_contains
from ..utils import nm_to_meter
Numeric = Union[int, float] Numeric = Union[int, float]
@ -73,6 +97,266 @@ def pairwise(iterable):
return zip(a, b) 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: class ConflictTheater:
terrain: Terrain terrain: Terrain
@ -83,17 +367,35 @@ class ConflictTheater:
land_poly = None # type: Polygon land_poly = None # type: Polygon
""" """
daytime_map: Dict[str, Tuple[int, int]] daytime_map: Dict[str, Tuple[int, int]]
frontline_data: Optional[Dict[str, ComplexFrontLine]] = None _frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
def __init__(self): def __init__(self):
self.controlpoints: List[ControlPoint] = [] 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]) self.land_poly = geometry.Polygon(self.landmap[0][0])
for x in self.landmap[1]: for x in self.landmap[1]:
self.land_poly = self.land_poly.difference(geometry.Polygon(x)) 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, def add_controlpoint(self, point: ControlPoint,
connected_to: Optional[List[ControlPoint]] = None): connected_to: Optional[List[ControlPoint]] = None):
if connected_to is None: if connected_to is None:
@ -153,11 +455,21 @@ class ConflictTheater:
def enemy_points(self) -> List[ControlPoint]: def enemy_points(self) -> List[ControlPoint]:
return [point for point in self.controlpoints if not point.captured] 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: def add_json_cp(self, theater, p: dict) -> ControlPoint:
if p["type"] == "airbase": if p["type"] == "airbase":
airbase = theater.terrain.airports[p["id"]].__class__ airbase = theater.terrain.airports[p["id"]]
if "radials" in p.keys(): if "radials" in p.keys():
radials = p["radials"] radials = p["radials"]
@ -188,7 +500,7 @@ class ConflictTheater:
return cp return cp
@staticmethod @staticmethod
def from_json(data: Dict[str, Any]) -> ConflictTheater: def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
theaters = { theaters = {
"Caucasus": CaucasusTheater, "Caucasus": CaucasusTheater,
"Nevada": NevadaTheater, "Nevada": NevadaTheater,
@ -199,6 +511,12 @@ class ConflictTheater:
} }
theater = theaters[data["theater"]] theater = theaters[data["theater"]]
t = theater() t = theater()
miz = data.get("miz", None)
if miz is not None:
MizCampaignLoader(directory / miz, t).populate_theater()
return t
cps = {} cps = {}
for p in data["player_points"]: for p in data["player_points"]:
cp = t.add_json_cp(theater, p) cp = t.add_json_cp(theater, p)
@ -376,10 +694,6 @@ class FrontLine(MissionTarget):
"""Returns a tuple of the two control points.""" """Returns a tuple of the two control points."""
return self.control_point_a, self.control_point_b return self.control_point_a, self.control_point_b
@property
def middle_point(self):
self.point_from_a(self.attack_distance / 2)
@property @property
def attack_distance(self): def attack_distance(self):
"""The total distance of all segments""" """The total distance of all segments"""

View File

@ -1,9 +1,12 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
import logging
import random
import re import re
from dataclasses import dataclass, field
from enum import Enum 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.mapping import Point
from dcs.ships import ( from dcs.ships import (
@ -13,6 +16,7 @@ from dcs.ships import (
Type_071_Amphibious_Transport_Dock, Type_071_Amphibious_Transport_Dock,
) )
from dcs.terrain.terrain import Airport from dcs.terrain.terrain import Airport
from dcs.unittype import FlyingType
from game import db from game import db
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
@ -20,12 +24,16 @@ from .base import Base
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .theatergroundobject import ( from .theatergroundobject import (
BaseDefenseGroundObject, BaseDefenseGroundObject,
EwrGroundObject,
SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
VehicleGroupGroundObject,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from ..event import UnitsDeliveryEvent
class ControlPointType(Enum): class ControlPointType(Enum):
@ -34,6 +42,87 @@ class ControlPointType(Enum):
LHA_GROUP = 2 # A group with a Tarawa carrier (Helicopters & Harrier) LHA_GROUP = 2 # A group with a Tarawa carrier (Helicopters & Harrier)
FARP = 4 # A FARP, with slots for helicopters FARP = 4 # A FARP, with slots for helicopters
FOB = 5 # A FOB (ground units only) 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): class ControlPoint(MissionTarget):
@ -57,6 +146,7 @@ class ControlPoint(MissionTarget):
self.at = at self.at = at
self.connected_objectives: List[TheaterGroundObject] = [] self.connected_objectives: List[TheaterGroundObject] = []
self.base_defenses: List[BaseDefenseGroundObject] = [] self.base_defenses: List[BaseDefenseGroundObject] = []
self.preset_locations = PresetLocations()
self.size = size self.size = size
self.importance = importance self.importance = importance
@ -69,6 +159,7 @@ class ControlPoint(MissionTarget):
self.cptype = cptype self.cptype = cptype
self.stances: Dict[int, CombatStance] = {} self.stances: Dict[int, CombatStance] = {}
self.airport = None self.airport = None
self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None
@property @property
def ground_objects(self) -> List[TheaterGroundObject]: 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): def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True):
assert airport assert airport
obj = cls(airport.id, airport.name, airport.position, airport, radials, size, importance, has_frontline, cptype=ControlPointType.AIRBASE) 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 return obj
@classmethod @classmethod
@ -144,7 +235,7 @@ class ControlPoint(MissionTarget):
return result return result
@property @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 :return: The maximum number of aircraft that can be stored in this control point
""" """
@ -157,7 +248,7 @@ class ControlPoint(MissionTarget):
else: else:
return 0 return 0
def connect(self, to): def connect(self, to: ControlPoint) -> None:
self.connected_points.append(to) self.connected_points.append(to)
self.stances[to.id] = CombatStance.DEFENSIVE self.stances[to.id] = CombatStance.DEFENSIVE
@ -222,6 +313,24 @@ class ControlPoint(MissionTarget):
def is_friendly(self, to_player: bool) -> bool: def is_friendly(self, to_player: bool) -> bool:
return self.captured == to_player 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: def capture(self, game: Game, for_player: bool) -> None:
if for_player: if for_player:
self.captured = True self.captured = True
@ -233,9 +342,8 @@ class ControlPoint(MissionTarget):
self.base.aircraft = {} self.base.aircraft = {}
self.base.armor = {} self.base.armor = {}
# Handle cyclic dependency. self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator from .start_generator import BaseDefenseGenerator
self.base_defenses = []
BaseDefenseGenerator(game, self).generate() BaseDefenseGenerator(game, self).generate()
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
@ -260,3 +368,41 @@ class ControlPoint(MissionTarget):
yield from [ yield from [
# TODO: FlightType.STRIKE # 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

View File

@ -1 +0,0 @@
"""Only here to keep compatibility for save games generated in version 2.2.0"""

View File

@ -4,7 +4,7 @@ import logging
import math import math
import pickle import pickle
import random import random
from typing import Any, Dict, List, Optional from typing import Any, Dict, Optional
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike from dcs.task import CAP, CAS, PinpointStrike
@ -13,6 +13,18 @@ from dcs.vehicles import AirDefence
from game import Game, db from game import Game, db
from game.factions.faction import Faction from game.factions.faction import Faction
from game.settings import Settings 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 game.version import VERSION
from gen import namegen from gen import namegen
from gen.defenses.armor_group_generator import generate_armor_group 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_lha_group,
generate_ship_group, generate_ship_group,
) )
from gen.locations.preset_location_finder import PresetLocationFinder from gen.locations.preset_location_finder import MizDataLocationFinder
from gen.locations.preset_locations import PresetLocation
from gen.missiles.missiles_group_generator import generate_missile_group from gen.missiles.missiles_group_generator import generate_missile_group
from gen.sam.sam_group_generator import ( from gen.sam.sam_group_generator import (
generate_anti_air_group, generate_anti_air_group,
generate_ewr_group, generate_shorad_group, generate_ewr_group, generate_shorad_group,
) )
from theater import ( from . import (
ConflictTheater, ConflictTheater,
ControlPoint, ControlPoint,
ControlPointType, ControlPointType,
TheaterGroundObject, OffMapSpawn,
)
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
from game.theater.theatergroundobject import (
EwrGroundObject,
SamGroundObject,
BuildingGroundObject,
CarrierGroundObject,
LhaGroundObject,
MissileSiteGroundObject,
ShipGroundObject,
VehicleGroupGroundObject,
) )
GroundObjectTemplates = Dict[str, Dict[str, Any]] GroundObjectTemplates = Dict[str, Dict[str, Any]]
@ -139,7 +139,13 @@ class GameGenerator:
control_point.base.commision_points = {} control_point.base.commision_points = {}
control_point.base.strength = 1 control_point.base.strength = 1
# The tasks here are confusing. PinpointStrike for some reason means
# ground units.
for task in [PinpointStrike, CAP, CAS, AirDefence]: 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: if IMPORTANCE_HIGH <= control_point.importance <= IMPORTANCE_LOW:
raise ValueError( raise ValueError(
f"CP importance must be between {IMPORTANCE_LOW} and " 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}) 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: class ControlPointGroundObjectGenerator:
def __init__(self, game: Game, control_point: ControlPoint) -> None: def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game self.game = game
self.control_point = control_point 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 @property
def faction_name(self) -> str: def faction_name(self) -> str:
@ -205,11 +355,9 @@ class ControlPointGroundObjectGenerator:
self.generate_ship() self.generate_ship()
def generate_ship(self) -> None: def generate_ship(self) -> None:
point = find_location(False, self.control_point.position, point = self.location_finder.location_for(
self.game.theater, 5000, 40000, [], False) LocationType.OffshoreStrikeTarget)
if point is None: if point is None:
logging.error(
f"Could not find point for {self.control_point}'s navy")
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -223,26 +371,10 @@ class ControlPointGroundObjectGenerator:
g.groups.append(group) g.groups.append(group)
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
def pick_preset_location(self, offshore=False) -> Optional[PresetLocation]:
""" class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
Return a preset location if any is setup and still available for this point def generate(self) -> bool:
@:param offshore Whether this should be an offshore location return True
@: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 CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator): class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
@ -299,6 +431,7 @@ class BaseDefenseGenerator:
def __init__(self, game: Game, control_point: ControlPoint) -> None: def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game self.game = game
self.control_point = control_point self.control_point = control_point
self.location_finder = LocationFinder(game, control_point)
@property @property
def faction_name(self) -> str: def faction_name(self) -> str:
@ -317,10 +450,8 @@ class BaseDefenseGenerator:
self.generate_base_defenses() self.generate_base_defenses()
def generate_ewr(self) -> None: def generate_ewr(self) -> None:
position = self._find_location() position = self.location_finder.location_for(LocationType.Ewr)
if position is None: if position is None:
logging.error("Could not find position for "
f"{self.control_point} EWR")
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -350,10 +481,8 @@ class BaseDefenseGenerator:
self.generate_garrison() self.generate_garrison()
def generate_garrison(self) -> None: def generate_garrison(self) -> None:
position = self._find_location() position = self.location_finder.location_for(LocationType.Garrison)
if position is None: if position is None:
logging.error("Could not find position for "
f"{self.control_point} garrison")
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -368,10 +497,9 @@ class BaseDefenseGenerator:
self.control_point.base_defenses.append(g) self.control_point.base_defenses.append(g)
def generate_sam(self) -> None: def generate_sam(self) -> None:
position = self._find_location() position = self.location_finder.location_for(
LocationType.BaseAirDefense)
if position is None: if position is None:
logging.error("Could not find position for "
f"{self.control_point} SAM")
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -385,10 +513,9 @@ class BaseDefenseGenerator:
self.control_point.base_defenses.append(g) self.control_point.base_defenses.append(g)
def generate_shorad(self) -> None: def generate_shorad(self) -> None:
position = self._find_location() position = self.location_finder.location_for(
LocationType.BaseAirDefense)
if position is None: if position is None:
logging.error("Could not find position for "
f"{self.control_point} SHORAD")
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -401,20 +528,6 @@ class BaseDefenseGenerator:
g.groups.append(group) g.groups.append(group)
self.control_point.base_defenses.append(g) 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): class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__(self, game: Game, control_point: ControlPoint, def __init__(self, game: Game, control_point: ControlPoint,
@ -442,15 +555,31 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
# Always generate at least one AA point. # Always generate at least one AA point.
self.generate_aa_site() self.generate_aa_site()
skip_sams = self.generate_required_aa()
# And between 2 and 7 other objectives. # And between 2 and 7 other objectives.
amount = random.randrange(2, 7) amount = random.randrange(2, 7)
for i in range(amount): for i in range(amount):
# 1 in 4 additional objectives are AA. # 1 in 4 additional objectives are AA.
if random.randint(0, 3) == 0: if random.randint(0, 3) == 0:
if skip_sams > 0:
skip_sams -= 1
else:
self.generate_aa_site() self.generate_aa_site()
else: else:
self.generate_ground_point() 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: def generate_ground_point(self) -> None:
try: try:
category = random.choice(self.faction.building_set) category = random.choice(self.faction.building_set)
@ -461,23 +590,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
obj_name = namegen.random_objective_name() obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values())) 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 # Pick from preset locations
location = self.pick_preset_location(offshore) point = self.location_finder.location_for(location_type)
# 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
if point is None: if point is None:
logging.error(
f"Could not find point for {obj_name} at {self.control_point}")
return return
object_id = 0 object_id = 0
@ -495,24 +615,12 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
def generate_aa_site(self) -> None: def generate_aa_site(self) -> None:
obj_name = namegen.random_objective_name() position = self.location_finder.location_for(LocationType.Sam)
# 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
if position is None: if position is None:
logging.error(
f"Could not find point for {obj_name} at {self.control_point}")
return return
self.generate_aa_at(position)
def generate_aa_at(self, position: Point) -> None:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id, g = SamGroundObject(namegen.random_objective_name(), group_id,
@ -527,22 +635,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.generate_missile_site() self.generate_missile_site()
def generate_missile_site(self) -> None: def generate_missile_site(self) -> None:
position = self.location_finder.location_for(LocationType.MissileSite)
# 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
if position is None: if position is None:
logging.info(
f"Could not find point for {self.control_point} missile site")
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -577,72 +671,9 @@ class GroundObjectGenerator:
generator = CarrierGroundObjectGenerator(self.game, control_point) generator = CarrierGroundObjectGenerator(self.game, control_point)
elif control_point.cptype == ControlPointType.LHA_GROUP: elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator(self.game, control_point) generator = LhaGroundObjectGenerator(self.game, control_point)
elif isinstance(control_point, OffMapSpawn):
generator = NoOpGroundObjectGenerator(self.game, control_point)
else: else:
generator = AirbaseGroundObjectGenerator(self.game, control_point, generator = AirbaseGroundObjectGenerator(self.game, control_point,
self.templates) self.templates)
return generator.generate() 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

View File

@ -243,8 +243,8 @@ class BaseDefenseGroundObject(TheaterGroundObject):
# TODO: Differentiate types. # TODO: Differentiate types.
# This type gets used both for AA sites (SAM, AAA, or SHORAD) but also for the # This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
# armor garrisons at airbases. These should each be split into their own types. # be split into their own types.
class SamGroundObject(BaseDefenseGroundObject): class SamGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, for_airbase: bool) -> None: control_point: ControlPoint, for_airbase: bool) -> None:

View File

@ -12,3 +12,7 @@ def meter_to_nm(value_in_meter: float) -> int:
def nm_to_meter(value_in_nm: float) -> int: def nm_to_meter(value_in_nm: float) -> int:
return int(value_in_nm * 1852) return int(value_in_nm * 1852)
def knots_to_kph(knots: float) -> int:
return int(knots * 1.852)

View File

@ -10,7 +10,7 @@ from typing import Optional
from dcs.weather import Weather as PydcsWeather, Wind from dcs.weather import Weather as PydcsWeather, Wind
from game.settings import Settings from game.settings import Settings
from theater import ConflictTheater from game.theater import ConflictTheater
class TimeOfDay(Enum): class TimeOfDay(Enum):

View File

@ -70,7 +70,13 @@ from dcs.unittype import FlyingType, UnitType
from game import db from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings 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.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit 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.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData from gen.runways import RunwayData
from theater import TheaterGroundObject
from game.theater.controlpoint import ControlPoint, ControlPointType
from .conflictgen import Conflict from .conflictgen import Conflict
from .flights.flightplan import ( from .flights.flightplan import (
CasFlightPlan, CasFlightPlan,
@ -92,7 +96,7 @@ from .flights.flightplan import (
PatrollingFlightPlan, PatrollingFlightPlan,
SweepFlightPlan, SweepFlightPlan,
) )
from .flights.traveltime import TotEstimator from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen from .naming import namegen
from .runways import RunwayAssigner from .runways import RunwayAssigner
@ -691,6 +695,18 @@ class AircraftConflictGenerator:
return StartType.Cold return StartType.Cold
return StartType.Warm 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], def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
package: Package, flight: Flight, package: Package, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
@ -748,19 +764,9 @@ class AircraftConflictGenerator:
channel = self.get_intra_flight_channel(unit_type) channel = self.get_intra_flight_channel(unit_type)
group.set_frequency(channel.mhz) group.set_frequency(channel.mhz)
# TODO: Support for different departure/arrival airfields. divert = None
cp = flight.from_cp if flight.divert is not None:
fallback_runway = RunwayData(cp.full_name, runway_heading=0, divert = self.determine_runway(flight.divert, dynamic_runways)
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
self.flights.append(FlightData( self.flights.append(FlightData(
package=package, package=package,
@ -770,10 +776,9 @@ class AircraftConflictGenerator:
friendly=flight.from_cp.captured, friendly=flight.from_cp.captured,
# Set later. # Set later.
departure_delay=timedelta(), departure_delay=timedelta(),
departure=departure_runway, departure=self.determine_runway(flight.departure, dynamic_runways),
arrival=departure_runway, arrival=self.determine_runway(flight.arrival, dynamic_runways),
# TODO: Support for divert airfields. divert=divert,
divert=None,
# Waypoints are added later, after they've had their TOTs set. # Waypoints are added later, after they've had their TOTs set.
waypoints=[], waypoints=[],
intra_flight_channel=channel intra_flight_channel=channel
@ -804,31 +809,37 @@ class AircraftConflictGenerator:
group_size=count, group_size=count,
parking_slots=None) parking_slots=None)
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, at: Point) -> FlyingGroup: def _generate_inflight(self, name: str, side: Country, flight: Flight,
assert count > 0 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 alt = WARM_START_HELI_ALT
speed = WARM_START_HELI_AIRSPEED
else: else:
alt = WARM_START_ALTITUDE 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)) 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( group = self.m.flight_group(
country=side, country=side,
name=name, name=name,
aircraft_type=unit_type, aircraft_type=flight.unit_type,
airport=None, airport=None,
position=pos, position=pos,
altitude=alt, altitude=alt,
speed=speed, speed=speed,
maintask=None, maintask=None,
group_size=count) group_size=flight.count)
group.points[0].alt_type = "RADIO" group.points[0].alt_type = alt_type
return group return group
def _generate_at_group(self, name: str, side: Country, def _generate_at_group(self, name: str, side: Country,
@ -974,9 +985,8 @@ class AircraftConflictGenerator:
group = self._generate_inflight( group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type), name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country, side=country,
unit_type=flight.unit_type, flight=flight,
count=flight.count, origin=cp)
at=cp.position)
elif cp.is_fleet: elif cp.is_fleet:
group_name = cp.get_carrier_group_name() group_name = cp.get_carrier_group_name()
group = self._generate_at_group( group = self._generate_at_group(
@ -1002,9 +1012,8 @@ class AircraftConflictGenerator:
group = self._generate_inflight( group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type), name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country, side=country,
unit_type=flight.unit_type, flight=flight,
count=flight.count, origin=cp)
at=cp.position)
group.points[0].alt = 1500 group.points[0].alt = 1500
return group return group

View File

@ -5,7 +5,8 @@ from typing import Tuple
from dcs.country import Country from dcs.country import Country
from dcs.mapping import Point 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 AIR_DISTANCE = 40000

View File

@ -16,11 +16,24 @@ from typing import (
Type, Type,
) )
from dcs.unittype import FlyingType, UnitType from dcs.unittype import FlyingType
from game import db from game import db
from game.data.radar_db import UNITS_WITH_RADAR from game.data.radar_db import UNITS_WITH_RADAR
from game.infos.information import Information 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 game.utils import nm_to_meter
from gen import Conflict from gen import Conflict
from gen.ato import Package from gen.ato import Package
@ -46,19 +59,6 @@ from gen.flights.flight import (
) )
from gen.flights.flightplan import FlightPlanBuilder from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator 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: if TYPE_CHECKING:
from game import Game from game import Game
@ -119,7 +119,7 @@ class AircraftAllocator:
def find_aircraft_for_flight( def find_aircraft_for_flight(
self, flight: ProposedFlight self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, UnitType]]: ) -> Optional[Tuple[ControlPoint, FlyingType]]:
"""Finds aircraft suitable for the given mission. """Finds aircraft suitable for the given mission.
Searches for aircraft capable of performing the given mission within the Searches for aircraft capable of performing the given mission within the
@ -190,7 +190,7 @@ class AircraftAllocator:
def find_aircraft_of_type( def find_aircraft_of_type(
self, flight: ProposedFlight, types: List[Type[FlyingType]], self, flight: ProposedFlight, types: List[Type[FlyingType]],
) -> Optional[Tuple[ControlPoint, UnitType]]: ) -> Optional[Tuple[ControlPoint, FlyingType]]:
airfields_in_range = self.closest_airfields.airfields_within( airfields_in_range = self.closest_airfields.airfields_within(
flight.max_distance flight.max_distance
) )
@ -214,6 +214,8 @@ class PackageBuilder:
global_inventory: GlobalAircraftInventory, global_inventory: GlobalAircraftInventory,
is_player: bool, is_player: bool,
start_type: str) -> None: start_type: str) -> None:
self.closest_airfields = closest_airfields
self.is_player = is_player
self.package = Package(location) self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory, self.allocator = AircraftAllocator(closest_airfields, global_inventory,
is_player) is_player)
@ -232,11 +234,32 @@ class PackageBuilder:
if assignment is None: if assignment is None:
return False return False
airfield, aircraft = assignment airfield, aircraft = assignment
flight = Flight(self.package, aircraft, plan.num_aircraft, airfield, if isinstance(airfield, OffMapSpawn):
plan.task, self.start_type) 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) self.package.add_flight(flight)
return True 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: def build(self) -> Package:
"""Returns the built package.""" """Returns the built package."""
return self.package return self.package
@ -406,6 +429,9 @@ class ObjectiveFinder:
CP. CP.
""" """
for cp in self.friendly_control_points(): 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_proximity = self.closest_airfields_to(cp)
airfields_in_threat_range = airfields_in_proximity.airfields_within( airfields_in_threat_range = airfields_in_proximity.airfields_within(
self.AIRFIELD_THREAT_RANGE self.AIRFIELD_THREAT_RANGE

View File

@ -1,7 +1,7 @@
"""Objective adjacency lists.""" """Objective adjacency lists."""
from typing import Dict, Iterator, List, Optional from typing import Dict, Iterator, List, Optional
from theater import ConflictTheater, ControlPoint, MissionTarget from game.theater import ConflictTheater, ControlPoint, MissionTarget
class ClosestAirfields: class ClosestAirfields:

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from enum import Enum 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.mapping import Point
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
@ -65,6 +65,7 @@ class FlightWaypointType(Enum):
INGRESS_DEAD = 20 INGRESS_DEAD = 20
INGRESS_SWEEP = 21 INGRESS_SWEEP = 21
INGRESS_BAI = 22 INGRESS_BAI = 22
DIVERT = 23
class FlightWaypoint: class FlightWaypoint:
@ -132,13 +133,16 @@ class FlightWaypoint:
class Flight: class Flight:
def __init__(self, package: Package, unit_type: FlyingType, count: int, def __init__(self, package: Package, unit_type: Type[FlyingType],
from_cp: ControlPoint, flight_type: FlightType, count: int, flight_type: FlightType, start_type: str,
start_type: str) -> None: departure: ControlPoint, arrival: ControlPoint,
divert: Optional[ControlPoint]) -> None:
self.package = package self.package = package
self.unit_type = unit_type self.unit_type = unit_type
self.count = count self.count = count
self.from_cp = from_cp self.departure = departure
self.arrival = arrival
self.divert = divert
self.flight_type = flight_type self.flight_type = flight_type
# TODO: Replace with FlightPlan. # TODO: Replace with FlightPlan.
self.targets: List[MissionTarget] = [] self.targets: List[MissionTarget] = []
@ -157,6 +161,10 @@ class Flight:
custom_waypoints=[] custom_waypoints=[]
) )
@property
def from_cp(self) -> ControlPoint:
return self.departure
@property @property
def points(self) -> List[FlightWaypoint]: def points(self) -> List[FlightWaypoint]:
return self.flight_plan.waypoints[1:] return self.flight_plan.waypoints[1:]

View File

@ -7,20 +7,19 @@ generating the waypoints for the mission.
""" """
from __future__ import annotations from __future__ import annotations
import math
from datetime import timedelta
from functools import cached_property
import logging import logging
import math
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta
from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.utils import nm_to_meter from game.theater import (
from theater import (
ControlPoint, ControlPoint,
FrontLine, FrontLine,
MissionTarget, MissionTarget,
@ -28,6 +27,7 @@ from theater import (
TheaterGroundObject, TheaterGroundObject,
) )
from game.theater.theatergroundobject import EwrGroundObject from game.theater.theatergroundobject import EwrGroundObject
from game.utils import nm_to_meter
from .closestairfields import ObjectiveDistanceCache from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime from .traveltime import GroundSpeed, TravelTime
@ -68,6 +68,10 @@ class FlightPlan:
@property @property
def waypoints(self) -> List[FlightWaypoint]: def waypoints(self) -> List[FlightWaypoint]:
"""A list of all waypoints in the flight plan, in order.""" """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 raise NotImplementedError
@property @property
@ -166,8 +170,7 @@ class FlightPlan:
class LoiterFlightPlan(FlightPlan): class LoiterFlightPlan(FlightPlan):
hold: FlightWaypoint hold: FlightWaypoint
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]:
raise NotImplementedError raise NotImplementedError
@property @property
@ -193,8 +196,7 @@ class FormationFlightPlan(LoiterFlightPlan):
join: FlightWaypoint join: FlightWaypoint
split: FlightWaypoint split: FlightWaypoint
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]:
raise NotImplementedError raise NotImplementedError
@property @property
@ -295,8 +297,7 @@ class PatrollingFlightPlan(FlightPlan):
return self.patrol_end_time return self.patrol_end_time
return None return None
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]:
raise NotImplementedError raise NotImplementedError
@property @property
@ -312,15 +313,17 @@ class PatrollingFlightPlan(FlightPlan):
class BarCapFlightPlan(PatrollingFlightPlan): class BarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint]
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from [
return [
self.takeoff, self.takeoff,
self.patrol_start, self.patrol_start,
self.patrol_end, self.patrol_end,
self.land, self.land,
] ]
if self.divert is not None:
yield self.divert
@dataclass(frozen=True) @dataclass(frozen=True)
@ -328,16 +331,18 @@ class CasFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
target: FlightWaypoint target: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint]
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from [
return [
self.takeoff, self.takeoff,
self.patrol_start, self.patrol_start,
self.target, self.target,
self.patrol_end, self.patrol_end,
self.land, self.land,
] ]
if self.divert is not None:
yield self.divert
def request_escort_at(self) -> Optional[FlightWaypoint]: def request_escort_at(self) -> Optional[FlightWaypoint]:
return self.patrol_start return self.patrol_start
@ -350,16 +355,18 @@ class CasFlightPlan(PatrollingFlightPlan):
class TarCapFlightPlan(PatrollingFlightPlan): class TarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint]
lead_time: timedelta lead_time: timedelta
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from [
return [
self.takeoff, self.takeoff,
self.patrol_start, self.patrol_start,
self.patrol_end, self.patrol_end,
self.land, self.land,
] ]
if self.divert is not None:
yield self.divert
@property @property
def tot_offset(self) -> timedelta: def tot_offset(self) -> timedelta:
@ -386,10 +393,6 @@ class TarCapFlightPlan(PatrollingFlightPlan):
return super().patrol_end_time return super().patrol_end_time
# TODO: Remove when breaking save compat.
FrontLineCapFlightPlan = TarCapFlightPlan
@dataclass(frozen=True) @dataclass(frozen=True)
class StrikeFlightPlan(FormationFlightPlan): class StrikeFlightPlan(FormationFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
@ -400,19 +403,23 @@ class StrikeFlightPlan(FormationFlightPlan):
egress: FlightWaypoint egress: FlightWaypoint
split: FlightWaypoint split: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint]
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from [
return [
self.takeoff, self.takeoff,
self.hold, self.hold,
self.join, self.join,
self.ingress self.ingress
] + self.targets + [ ]
yield from self.targets
yield from[
self.egress, self.egress,
self.split, self.split,
self.land, self.land,
] ]
if self.divert is not None:
yield self.divert
@property @property
def package_speed_waypoints(self) -> Set[FlightWaypoint]: def package_speed_waypoints(self) -> Set[FlightWaypoint]:
@ -511,17 +518,19 @@ class SweepFlightPlan(LoiterFlightPlan):
sweep_start: FlightWaypoint sweep_start: FlightWaypoint
sweep_end: FlightWaypoint sweep_end: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint]
lead_time: timedelta lead_time: timedelta
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from [
return [
self.takeoff, self.takeoff,
self.hold, self.hold,
self.sweep_start, self.sweep_start,
self.sweep_end, self.sweep_end,
self.land, self.land,
] ]
if self.divert is not None:
yield self.divert
@property @property
def tot_waypoint(self) -> Optional[FlightWaypoint]: def tot_waypoint(self) -> Optional[FlightWaypoint]:
@ -567,9 +576,8 @@ class SweepFlightPlan(LoiterFlightPlan):
class CustomFlightPlan(FlightPlan): class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint] custom_waypoints: List[FlightWaypoint]
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from self.custom_waypoints
return self.custom_waypoints
@property @property
def tot_waypoint(self) -> Optional[FlightWaypoint]: def tot_waypoint(self) -> Optional[FlightWaypoint]:
@ -774,10 +782,11 @@ class FlightPlanBuilder:
package=self.package, package=self.package,
flight=flight, flight=flight,
patrol_duration=self.doctrine.cap_duration, patrol_duration=self.doctrine.cap_duration,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.departure),
patrol_start=start, patrol_start=start,
patrol_end=end, 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: def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
@ -800,11 +809,12 @@ class FlightPlanBuilder:
package=self.package, package=self.package,
flight=flight, flight=flight,
lead_time=timedelta(minutes=5), lead_time=timedelta(minutes=5),
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)), hold=builder.hold(self._hold_point(flight)),
sweep_start=start, sweep_start=start,
sweep_end=end, sweep_end=end,
land=builder.land(flight.from_cp) land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
) )
def racetrack_for_objective(self, def racetrack_for_objective(self,
@ -900,10 +910,11 @@ class FlightPlanBuilder:
# requests an escort the CAP flight will remain on station for the # requests an escort the CAP flight will remain on station for the
# duration of the escorted mission, or until it is winchester/bingo. # duration of the escorted mission, or until it is winchester/bingo.
patrol_duration=self.doctrine.cap_duration, patrol_duration=self.doctrine.cap_duration,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.departure),
patrol_start=start, patrol_start=start,
patrol_end=end, 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, def generate_dead(self, flight: Flight,
@ -965,14 +976,15 @@ class FlightPlanBuilder:
return StrikeFlightPlan( return StrikeFlightPlan(
package=self.package, package=self.package,
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)), hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join), join=builder.join(self.package.waypoints.join),
ingress=ingress, ingress=ingress,
targets=[target], targets=[target],
egress=egress, egress=egress,
split=builder.split(self.package.waypoints.split), 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: def generate_cas(self, flight: Flight) -> CasFlightPlan:
@ -999,11 +1011,12 @@ class FlightPlanBuilder:
package=self.package, package=self.package,
flight=flight, flight=flight,
patrol_duration=self.doctrine.cas_duration, patrol_duration=self.doctrine.cas_duration,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.departure),
patrol_start=builder.ingress_cas(ingress, location), patrol_start=builder.ingress_cas(ingress, location),
target=builder.cas(center), target=builder.cas(center),
patrol_end=builder.egress(egress, location), patrol_end=builder.egress(egress, location),
land=builder.land(flight.from_cp) land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
) )
@staticmethod @staticmethod
@ -1030,7 +1043,7 @@ class FlightPlanBuilder:
def _hold_point(self, flight: Flight) -> Point: def _hold_point(self, flight: Flight) -> Point:
assert self.package.waypoints is not None assert self.package.waypoints is not None
origin = flight.from_cp.position origin = flight.departure.position
target = self.package.target.position target = self.package.target.position
join = self.package.waypoints.join join = self.package.waypoints.join
origin_to_target = origin.distance_to_point(target) origin_to_target = origin.distance_to_point(target)
@ -1118,14 +1131,15 @@ class FlightPlanBuilder:
return StrikeFlightPlan( return StrikeFlightPlan(
package=self.package, package=self.package,
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)), hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join), join=builder.join(self.package.waypoints.join),
ingress=ingress, ingress=ingress,
targets=target_waypoints, targets=target_waypoints,
egress=builder.egress(self.package.waypoints.egress, location), egress=builder.egress(self.package.waypoints.egress, location),
split=builder.split(self.package.waypoints.split), 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: def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
@ -1201,7 +1215,7 @@ class FlightPlanBuilder:
) )
for airfield in cache.closest_airfields: for airfield in cache.closest_airfields:
for flight in self.package.flights: for flight in self.package.flights:
if flight.from_cp == airfield: if flight.departure == airfield:
return airfield return airfield
raise RuntimeError( raise RuntimeError(
"Could not find any airfield assigned to this package" "Could not find any airfield assigned to this package"

View File

@ -45,20 +45,21 @@ class GroundSpeed:
return int(cls.from_mach(mach, altitude)) # knots return int(cls.from_mach(mach, altitude)) # knots
@staticmethod @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. """Returns the ground speed in knots for the given mach and altitude.
Args: Args:
mach: The mach number to convert to ground speed. mach: The mach number to convert to ground speed.
altitude: The altitude in feet. altitude_m: The altitude in meters.
Returns: Returns:
The ground speed corresponding to the given altitude and mach number The ground speed corresponding to the given altitude and mach number
in knots. in knots.
""" """
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html # https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
if altitude <= 36152: altitude_ft = altitude_m * 3.28084
temperature_f = 59 - 0.00356 * altitude if altitude_ft <= 36152:
temperature_f = 59 - 0.00356 * altitude_ft
else: else:
# There's another formula for altitudes over 82k feet, but we better # There's another formula for altitudes over 82k feet, but we better
# not be planning waypoints that high... # not be planning waypoints that high...

View File

@ -8,11 +8,14 @@ from dcs.unit import Unit
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
from game.data.doctrine import Doctrine 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 game.weather import Conditions
from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightWaypoint, FlightWaypointType
from ..runways import RunwayAssigner
@dataclass(frozen=True) @dataclass(frozen=True)
@ -34,8 +37,7 @@ class WaypointBuilder:
def is_helo(self) -> bool: def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False) return getattr(self.flight.unit_type, "helicopter", False)
@staticmethod def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
def takeoff(departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier. """Create takeoff waypoint for the given arrival airfield or carrier.
Note that the takeoff waypoint will automatically be created by pydcs Note that the takeoff waypoint will automatically be created by pydcs
@ -46,6 +48,18 @@ class WaypointBuilder:
departure: Departure airfield or carrier. departure: Departure airfield or carrier.
""" """
position = departure.position position = departure.position
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( waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF, FlightWaypointType.TAKEOFF,
position.x, position.x,
@ -58,14 +72,25 @@ class WaypointBuilder:
waypoint.pretty_name = "Takeoff" waypoint.pretty_name = "Takeoff"
return waypoint return waypoint
@staticmethod def land(self, arrival: ControlPoint) -> FlightWaypoint:
def land(arrival: ControlPoint) -> FlightWaypoint:
"""Create descent waypoint for the given arrival airfield or carrier. """Create descent waypoint for the given arrival airfield or carrier.
Args: Args:
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
position = arrival.position 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( waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT, FlightWaypointType.LANDING_POINT,
position.x, position.x,
@ -78,6 +103,40 @@ class WaypointBuilder:
waypoint.pretty_name = "Land" waypoint.pretty_name = "Land"
return waypoint 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.DIVERT,
position.x,
position.y,
altitude
)
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: def hold(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.LOITER, FlightWaypointType.LOITER,

View File

@ -2,12 +2,12 @@ import random
from enum import Enum from enum import Enum
from typing import Dict, List from typing import Dict, List
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
import pydcs_extensions.frenchpack.frenchpack as frenchpack import pydcs_extensions.frenchpack.frenchpack as frenchpack
from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
TYPE_TANKS = [ TYPE_TANKS = [
Armor.MBT_T_55, Armor.MBT_T_55,

View File

@ -20,14 +20,14 @@ from dcs.task import (
EPLRS, EPLRS,
OptAlarmState, OptAlarmState,
) )
from dcs.unit import Ship, Vehicle, Unit from dcs.unit import Ship, Unit, Vehicle
from dcs.unitgroup import Group, ShipGroup, StaticGroup from dcs.unitgroup import Group, ShipGroup, StaticGroup
from dcs.unittype import StaticType, UnitType from dcs.unittype import StaticType, UnitType
from game import db from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name from game.db import unit_type_from_name
from theater import ControlPoint, TheaterGroundObject from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, CarrierGroundObject, BuildingGroundObject, CarrierGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,

View File

@ -8,7 +8,7 @@ from gen.locations.preset_control_point_locations import PresetControlPointLocat
from gen.locations.preset_locations import PresetLocation from gen.locations.preset_locations import PresetLocation
class PresetLocationFinder: class MizDataLocationFinder:
@staticmethod @staticmethod
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations: def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:

View File

@ -134,7 +134,7 @@ RADIOS: List[Radio] = [
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)), Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
# MiG-21bis # MiG-21bis
Radio("RSIU-5V", MHz(100), MHz(150), step=MHz(1)), Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
# Ka-50 # Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps. # Note: Also capable of 100MHz-150MHz, but we can't model gaps.

View File

@ -7,8 +7,8 @@ from typing import Iterator, Optional
from dcs.terrain.terrain import Airport from dcs.terrain.terrain import Airport
from game.theater import ControlPoint, ControlPointType
from game.weather import Conditions from game.weather import Conditions
from theater import ControlPoint, ControlPointType
from .airfields import AIRFIELD_DATA from .airfields import AIRFIELD_DATA
from .radios import RadioFrequency from .radios import RadioFrequency
from .tacan import TacanChannel from .tacan import TacanChannel

View File

@ -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("<b>Parking Slots :</b>"), 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)

View File

@ -3,13 +3,13 @@ from typing import Iterable
from PySide2.QtWidgets import QComboBox from PySide2.QtWidgets import QComboBox
from dcs.planes import PlaneType from dcs.unittype import FlyingType
class QAircraftTypeSelector(QComboBox): class QAircraftTypeSelector(QComboBox):
"""Combo box for selecting among the given aircraft types.""" """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__() super().__init__()
for aircraft in aircraft_types: for aircraft in aircraft_types:
self.addItem(f"{aircraft.id}", userData=aircraft) self.addItem(f"{aircraft.id}", userData=aircraft)

View File

@ -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()

View File

@ -2,7 +2,7 @@
from PySide2.QtWidgets import QComboBox from PySide2.QtWidgets import QComboBox
from theater import ConflictTheater, MissionTarget from game.theater import ConflictTheater, MissionTarget
class QFlightTypeComboBox(QComboBox): class QFlightTypeComboBox(QComboBox):

View File

@ -3,7 +3,7 @@ from typing import Iterable
from PySide2.QtCore import Signal from PySide2.QtCore import Signal
from PySide2.QtWidgets import QComboBox from PySide2.QtWidgets import QComboBox
from dcs.planes import PlaneType from dcs.unittype import FlyingType
from game.inventory import GlobalAircraftInventory from game.inventory import GlobalAircraftInventory
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
@ -20,7 +20,7 @@ class QOriginAirfieldSelector(QComboBox):
def __init__(self, global_inventory: GlobalAircraftInventory, def __init__(self, global_inventory: GlobalAircraftInventory,
origins: Iterable[ControlPoint], origins: Iterable[ControlPoint],
aircraft: PlaneType) -> None: aircraft: FlyingType) -> None:
super().__init__() super().__init__()
self.global_inventory = global_inventory self.global_inventory = global_inventory
self.origins = list(origins) self.origins = list(origins)
@ -28,7 +28,7 @@ class QOriginAirfieldSelector(QComboBox):
self.rebuild_selector() self.rebuild_selector()
self.currentIndexChanged.connect(self.index_changed) self.currentIndexChanged.connect(self.index_changed)
def change_aircraft(self, aircraft: PlaneType) -> None: def change_aircraft(self, aircraft: FlyingType) -> None:
if self.aircraft == aircraft: if self.aircraft == aircraft:
return return
self.aircraft = aircraft self.aircraft = aircraft

View File

@ -1,10 +1,10 @@
from PySide2.QtGui import QStandardItem, QStandardItemModel from PySide2.QtGui import QStandardItem, QStandardItemModel
from game import Game from game import Game
from game.theater import ControlPointType
from gen import BuildingGroundObject, Conflict, FlightWaypointType from gen import BuildingGroundObject, Conflict, FlightWaypointType
from gen.flights.flight import FlightWaypoint from gen.flights.flight import FlightWaypoint
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
from theater import ControlPointType
class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):

View File

@ -13,11 +13,11 @@ from PySide2.QtWidgets import (
) )
import qt_ui.uiconstants as const import qt_ui.uiconstants as const
from game.theater import FrontLine
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
from theater import FrontLine
class QFrontLine(QGraphicsLineItem): class QFrontLine(QGraphicsLineItem):

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import datetime import datetime
import logging import logging
import math 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.QtCore import QPointF, Qt
from PySide2.QtGui import ( from PySide2.QtGui import (
@ -27,6 +27,13 @@ from dcs.mapping import point_from_heading
import qt_ui.uiconstants as CONST import qt_ui.uiconstants as CONST
from game import Game, db 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.utils import meter_to_feet
from game.weather import TimeOfDay from game.weather import TimeOfDay
from gen import Conflict 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.QMapControlPoint import QMapControlPoint
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal 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: def binomial(i: int, n: int) -> float:
"""Binomial coefficient""" """Binomial coefficient"""
@ -373,6 +374,10 @@ class QLiberationMap(QGraphicsView):
FlightWaypointType.TARGET_SHIP, FlightWaypointType.TARGET_SHIP,
) )
for idx, point in enumerate(flight.flight_plan.waypoints[1:]): 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)) new_pos = self._transform_point(Point(point.x, point.y))
self.draw_flight_path(scene, prev_pos, new_pos, is_player, self.draw_flight_path(scene, prev_pos, new_pos, is_player,
selected) selected)
@ -386,7 +391,6 @@ class QLiberationMap(QGraphicsView):
self.draw_waypoint_info(scene, idx + 1, point, new_pos, self.draw_waypoint_info(scene, idx + 1, point, new_pos,
flight.flight_plan) flight.flight_plan)
prev_pos = tuple(new_pos) 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], def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
player: bool, selected: bool) -> None: player: bool, selected: bool) -> None:

View File

@ -4,9 +4,9 @@ from PySide2.QtGui import QColor, QPainter
from PySide2.QtWidgets import QAction, QMenu from PySide2.QtWidgets import QAction, QMenu
import qt_ui.uiconstants as const import qt_ui.uiconstants as const
from game.theater import ControlPoint
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2 from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from theater import ControlPoint
from .QMapObject import QMapObject from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions from ...displayoptions import DisplayOptions
from ...windows.GameUpdateSignal import GameUpdateSignal from ...windows.GameUpdateSignal import GameUpdateSignal
@ -79,11 +79,8 @@ class QMapControlPoint(QMapObject):
for connected in self.control_point.connected_points: for connected in self.control_point.connected_points:
if connected.captured: if connected.captured:
break
else:
return
menu.addAction(self.capture_action) menu.addAction(self.capture_action)
break
def cheat_capture(self) -> None: def cheat_capture(self) -> None:
self.control_point.capture(self.game_model.game, for_player=True) self.control_point.capture(self.game_model.game, for_player=True)

View File

@ -8,8 +8,8 @@ import qt_ui.uiconstants as const
from game import Game from game import Game
from game.data.building_data import FORTIFICATION_BUILDINGS from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import REWARDS from game.db import REWARDS
from game.theater import ControlPoint, TheaterGroundObject
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from theater import ControlPoint, TheaterGroundObject
from .QMapObject import QMapObject from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions from ...displayoptions import DisplayOptions

View File

@ -47,6 +47,9 @@ class QMapObject(QGraphicsRectItem):
object_details_action.triggered.connect(self.on_click) object_details_action.triggered.connect(self.on_click)
menu.addAction(object_details_action) menu.addAction(object_details_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 = QAction(f"New package")
new_package_action.triggered.connect(self.open_new_package_dialog) new_package_action.triggered.connect(self.open_new_package_dialog)
menu.addAction(new_package_action) menu.addAction(new_package_action)

View File

@ -2,12 +2,12 @@ from PySide2.QtCore import Qt
from PySide2.QtGui import QCloseEvent, QPixmap from PySide2.QtGui import QCloseEvent, QPixmap
from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget
from game.theater import ControlPoint, ControlPointType
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
from theater import ControlPoint, ControlPointType
class QBaseMenu2(QDialog): class QBaseMenu2(QDialog):
@ -18,7 +18,6 @@ class QBaseMenu2(QDialog):
# Attrs # Attrs
self.cp = cp self.cp = cp
self.game_model = game_model self.game_model = game_model
self.is_carrier = self.cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
self.objectName = "menuDialogue" self.objectName = "menuDialogue"
# Widgets # Widgets
@ -58,7 +57,7 @@ class QBaseMenu2(QDialog):
title = QLabel("<b>" + self.cp.name + "</b>") title = QLabel("<b>" + self.cp.name + "</b>")
title.setAlignment(Qt.AlignLeft | Qt.AlignTop) title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title") 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")) "Available" if self.cp.has_runway() else "Unavailable"))
self.topLayout.addWidget(title) self.topLayout.addWidget(title)
self.topLayout.addWidget(unitsPower) self.topLayout.addWidget(unitsPower)

View File

@ -1,22 +1,20 @@
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.models import GameModel
from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand
from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ 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.ground_forces.QGroundForcesHQ import QGroundForcesHQ
from qt_ui.windows.basemenu.intel.QIntelInfo import QIntelInfo from qt_ui.windows.basemenu.intel.QIntelInfo import QIntelInfo
from theater import ControlPoint
class QBaseMenuTabs(QTabWidget): class QBaseMenuTabs(QTabWidget):
def __init__(self, cp: ControlPoint, game_model: GameModel): def __init__(self, cp: ControlPoint, game_model: GameModel):
super(QBaseMenuTabs, self).__init__() super(QBaseMenuTabs, self).__init__()
self.cp = cp
if cp:
if not cp.captured: if not cp.captured:
if not cp.is_carrier: if not cp.is_carrier and not isinstance(cp, OffMapSpawn):
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Base Defenses") self.addTab(self.base_defenses_hq, "Base Defenses")
self.intel = QIntelInfo(cp, game_model.game) self.intel = QIntelInfo(cp, game_model.game)
@ -26,18 +24,11 @@ class QBaseMenuTabs(QTabWidget):
self.airfield_command = QAirfieldCommand(cp, game_model) self.airfield_command = QAirfieldCommand(cp, game_model)
self.addTab(self.airfield_command, "Airfield Command") self.addTab(self.airfield_command, "Airfield Command")
if not cp.is_carrier: 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.ground_forces_hq = QGroundForcesHQ(cp, game_model)
self.addTab(self.ground_forces_hq, "Ground Forces HQ") self.addTab(self.ground_forces_hq, "Ground Forces HQ")
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Base Defenses") 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")
else:
tabError = QFrame()
l = QGridLayout()
l.addWidget(QLabel("No Control Point"))
tabError.setLayout(l)
self.addTab(tabError, "No Control Point")

View File

@ -1,3 +1,6 @@
import logging
from typing import Type
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QGroupBox, QGroupBox,
QHBoxLayout, QHBoxLayout,
@ -6,17 +9,17 @@ from PySide2.QtWidgets import (
QSizePolicy, QSizePolicy,
QSpacerItem, QSpacerItem,
) )
import logging
from dcs.unittype import UnitType 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: class QRecruitBehaviour:
game = None game_model: GameModel
cp = None cp: ControlPoint
deliveryEvent = None
existing_units_labels = None existing_units_labels = None
bought_amount_labels = None bought_amount_labels = None
maximum_units = -1 maximum_units = -1
@ -24,12 +27,16 @@ class QRecruitBehaviour:
BUDGET_FORMAT = "Available Budget: <b>${}M</b>" BUDGET_FORMAT = "Available Budget: <b>${}M</b>"
def __init__(self) -> None: def __init__(self) -> None:
self.deliveryEvent = None
self.bought_amount_labels = {} self.bought_amount_labels = {}
self.existing_units_labels = {} self.existing_units_labels = {}
self.recruitable_types = [] self.recruitable_types = []
self.update_available_budget() self.update_available_budget()
@property
def pending_deliveries(self) -> UnitsDeliveryEvent:
assert self.cp.pending_unit_deliveries
return self.cp.pending_unit_deliveries
@property @property
def budget(self) -> int: def budget(self) -> int:
return self.game_model.game.budget return self.game_model.game.budget
@ -47,7 +54,7 @@ class QRecruitBehaviour:
exist.setLayout(existLayout) exist.setLayout(existLayout)
existing_units = self.cp.base.total_units_of_type(unit_type) 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("<b>" + db.unit_type_name_2(unit_type) + "</b>") unitName = QLabel("<b>" + db.unit_type_name_2(unit_type) + "</b>")
unitName.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) unitName.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
@ -100,10 +107,10 @@ class QRecruitBehaviour:
return row + 1 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("<b>{}</b>".format( self.bought_amount_labels[unit_type].setText("<b>{}</b>".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("<b>{}</b>".format( self.existing_units_labels[unit_type].setText("<b>{}</b>".format(
@ -119,17 +126,10 @@ class QRecruitBehaviour:
child.setText( child.setText(
QRecruitBehaviour.BUDGET_FORMAT.format(self.budget)) QRecruitBehaviour.BUDGET_FORMAT.format(self.budget))
def buy(self, unit_type): def buy(self, unit_type: Type[UnitType]):
if self.maximum_units > 0:
if self.total_units + 1 > self.maximum_units:
logging.info("Not enough space left !")
# TODO : display modal warning
return
price = db.PRICES[unit_type] price = db.PRICES[unit_type]
if self.budget >= price: if self.budget >= price:
self.deliveryEvent.deliver({unit_type: 1}) self.pending_deliveries.deliver({unit_type: 1})
self.budget -= price self.budget -= price
else: else:
# TODO : display modal warning # TODO : display modal warning
@ -138,12 +138,12 @@ class QRecruitBehaviour:
self.update_available_budget() self.update_available_budget()
def sell(self, unit_type): 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] price = db.PRICES[unit_type]
self.budget += price self.budget += price
self.deliveryEvent.units[unit_type] = self.deliveryEvent.units[unit_type] - 1 self.pending_deliveries.units[unit_type] = self.pending_deliveries.units[unit_type] - 1
if self.deliveryEvent.units[unit_type] == 0: if self.pending_deliveries.units[unit_type] == 0:
del self.deliveryEvent.units[unit_type] del self.pending_deliveries.units[unit_type]
elif self.cp.base.total_units_of_type(unit_type) > 0: elif self.cp.base.total_units_of_type(unit_type) > 0:
price = db.PRICES[unit_type] price = db.PRICES[unit_type]
self.budget += price self.budget += price
@ -152,25 +152,6 @@ class QRecruitBehaviour:
self._update_count_label(unit_type) self._update_count_label(unit_type)
self.update_available_budget() 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): def set_maximum_units(self, maximum_units):
""" """
Set the maximum number of units that can be bought Set the maximum number of units that can be bought

View File

@ -1,3 +1,4 @@
import logging
from typing import Optional, Set from typing import Optional, Set
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
@ -11,13 +12,14 @@ from PySide2.QtWidgets import (
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
) )
from dcs.task import CAP, CAS
from dcs.unittype import UnitType 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.models import GameModel
from qt_ui.uiconstants import ICONS from qt_ui.uiconstants import ICONS
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
from theater import CAP, CAS, ControlPoint, db
class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
@ -25,25 +27,18 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
QFrame.__init__(self) QFrame.__init__(self)
self.cp = cp self.cp = cp
self.game_model = game_model self.game_model = game_model
self.deliveryEvent: Optional[UnitsDeliveryEvent] = None
self.bought_amount_labels = {} self.bought_amount_labels = {}
self.existing_units_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 # 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.set_recruitable_types([CAP, CAS])
self.bought_amount_labels = {} self.bought_amount_labels = {}
self.existing_units_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() self.init_ui()
@ -86,13 +81,18 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
self.setLayout(main_layout) self.setLayout(main_layout)
def buy(self, unit_type): 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) 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): def sell(self, unit_type: UnitType):
# Don't need to remove aircraft from the inventory if we're canceling # Don't need to remove aircraft from the inventory if we're canceling
# orders. # 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 global_inventory = self.game_model.game.aircraft_inventory
inventory = global_inventory.for_control_point(self.cp) inventory = global_inventory.for_control_point(self.cp)
try: try:
@ -105,22 +105,26 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
"assigned to a mission?", QMessageBox.Ok) "assigned to a mission?", QMessageBox.Ok)
return return
super().sell(unit_type) 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): class QHangarStatus(QHBoxLayout):
def __init__(self, current_amount: int, max_amount: int): def __init__(self, control_point: ControlPoint) -> None:
super(QHangarStatus, self).__init__() super().__init__()
self.control_point = control_point
self.icon = QLabel() self.icon = QLabel()
self.icon.setPixmap(ICONS["Hangar"]) self.icon.setPixmap(ICONS["Hangar"])
self.text = QLabel("") self.text = QLabel("")
self.update_label(current_amount, max_amount) self.update_label()
self.addWidget(self.icon, Qt.AlignLeft) self.addWidget(self.icon, Qt.AlignLeft)
self.addWidget(self.text, Qt.AlignLeft) self.addWidget(self.text, Qt.AlignLeft)
self.addStretch(50) self.addStretch(50)
self.setAlignment(Qt.AlignLeft) self.setAlignment(Qt.AlignLeft)
def update_label(self, current_amount: int, max_amount: int): def update_label(self) -> None:
self.text.setText("<strong>{}/{}</strong>".format(current_amount, max_amount)) current_amount = self.control_point.expected_aircraft_next_turn
max_amount = self.control_point.total_aircraft_parking
self.text.setText(f"<strong>{current_amount}/{max_amount}</strong>")

View File

@ -1,10 +1,10 @@
from PySide2.QtWidgets import QFrame, QGridLayout, QGroupBox, QVBoxLayout from PySide2.QtWidgets import QFrame, QGridLayout, QGroupBox, QVBoxLayout
from game.theater import ControlPoint
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import \ from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import \
QAircraftRecruitmentMenu QAircraftRecruitmentMenu
from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView
from theater import ControlPoint
class QAirfieldCommand(QFrame): class QAirfieldCommand(QFrame):

View File

@ -1,10 +1,16 @@
from PySide2.QtCore import Qt 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.dialogs import Dialog
from qt_ui.uiconstants import VEHICLES_ICONS from qt_ui.uiconstants import VEHICLES_ICONS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from theater import ControlPoint, TheaterGroundObject
class QBaseDefenseGroupInfo(QGroupBox): class QBaseDefenseGroupInfo(QGroupBox):

View File

@ -1,7 +1,9 @@
from PySide2.QtWidgets import QFrame, QGridLayout from PySide2.QtWidgets import QFrame, QGridLayout
from game import Game from game import Game
from qt_ui.windows.basemenu.base_defenses.QBaseInformation import QBaseInformation from game.theater import ControlPoint
from theater import ControlPoint from qt_ui.windows.basemenu.base_defenses.QBaseInformation import \
QBaseInformation
class QBaseDefensesHQ(QFrame): class QBaseDefensesHQ(QFrame):

View File

@ -1,10 +1,15 @@
from PySide2.QtGui import Qt 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 game.theater import Airport, ControlPoint
from qt_ui.uiconstants import AIRCRAFT_ICONS, VEHICLES_ICONS from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import \
from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import QBaseDefenseGroupInfo QBaseDefenseGroupInfo
from theater import ControlPoint, Airport
class QBaseInformation(QFrame): class QBaseInformation(QFrame):

View File

@ -6,11 +6,12 @@ from PySide2.QtWidgets import (
QVBoxLayout, QVBoxLayout,
QWidget, 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.models import GameModel
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
from theater import ControlPoint, PinpointStrike, db
class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
@ -23,12 +24,6 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
self.bought_amount_labels = {} self.bought_amount_labels = {}
self.existing_units_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() self.init_ui()
def init_ui(self): def init_ui(self):

View File

@ -1,11 +1,11 @@
from PySide2.QtWidgets import QFrame, QGridLayout from PySide2.QtWidgets import QFrame, QGridLayout
from game.theater import ControlPoint
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import \ from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import \
QArmorRecruitmentMenu QArmorRecruitmentMenu
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import \ from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import \
QGroundForcesStrategy QGroundForcesStrategy
from theater import ControlPoint
class QGroundForcesHQ(QFrame): class QGroundForcesHQ(QFrame):

View File

@ -1,8 +1,9 @@
from PySide2.QtWidgets import QLabel, QGroupBox, QVBoxLayout from PySide2.QtWidgets import QGroupBox, QLabel, QVBoxLayout
from game import Game from game import Game
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import QGroundForcesStrategySelector from game.theater import ControlPoint
from theater import ControlPoint from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import \
QGroundForcesStrategySelector
class QGroundForcesStrategy(QGroupBox): class QGroundForcesStrategy(QGroupBox):

View File

@ -1,6 +1,6 @@
from PySide2.QtWidgets import QComboBox from PySide2.QtWidgets import QComboBox
from theater import ControlPoint, CombatStance from game.theater import CombatStance, ControlPoint
class QGroundForcesStrategySelector(QComboBox): class QGroundForcesStrategySelector(QComboBox):

View File

@ -1,11 +1,14 @@
from PySide2.QtWidgets import (
QFrame,
QGridLayout,
QGroupBox,
QLabel,
QVBoxLayout,
)
from dcs.task import CAP, CAS, Embarking, PinpointStrike
from game import Game, db
from PySide2.QtWidgets import QLabel, QGroupBox, QVBoxLayout, QFrame, QGridLayout from game.theater import ControlPoint
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
class QIntelInfo(QFrame): class QIntelInfo(QFrame):

View File

@ -2,7 +2,7 @@ import os
from PySide2.QtGui import QPixmap from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QVBoxLayout, QLabel from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QVBoxLayout, QLabel
from game.db import REWARDS
class QBuildingInfo(QGroupBox): class QBuildingInfo(QGroupBox):
@ -28,6 +28,13 @@ class QBuildingInfo(QGroupBox):
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addWidget(self.header) layout.addWidget(self.header)
layout.addWidget(self.name) 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 = '<s>' + income_label_text + '</s>'
self.reward = QLabel(income_label_text)
layout.addWidget(self.reward)
footer = QHBoxLayout() footer = QHBoxLayout()
self.setLayout(layout) self.setLayout(layout)

View File

@ -2,20 +2,31 @@ import logging
from PySide2 import QtCore from PySide2 import QtCore
from PySide2.QtGui import Qt from PySide2.QtGui import Qt
from PySide2.QtWidgets import QHBoxLayout, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton, \ from PySide2.QtWidgets import (
QComboBox, QSpinBox, QMessageBox QComboBox,
QDialog,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QSpinBox,
QVBoxLayout,
)
from dcs import Point from dcs import Point
from game import Game, db from game import Game, db
from game.data.building_data import FORTIFICATION_BUILDINGS from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import PRICES, unit_type_of, PinpointStrike from game.db import PRICES, PinpointStrike, REWARDS, unit_type_of
from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size 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 gen.sam.sam_group_generator import get_faction_possible_sams_generator
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QBudgetBox import QBudgetBox from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo
from theater import ControlPoint, TheaterGroundObject
class QGroundObjectMenu(QDialog): class QGroundObjectMenu(QDialog):
@ -51,6 +62,8 @@ class QGroundObjectMenu(QDialog):
self.mainLayout.addWidget(self.intelBox) self.mainLayout.addWidget(self.intelBox)
else: else:
self.mainLayout.addWidget(self.buildingBox) self.mainLayout.addWidget(self.buildingBox)
if self.cp.captured:
self.mainLayout.addWidget(self.financesBox)
self.actionLayout = QHBoxLayout() self.actionLayout = QHBoxLayout()
@ -104,12 +117,26 @@ class QGroundObjectMenu(QDialog):
self.buildingBox = QGroupBox("Buildings :") self.buildingBox = QGroupBox("Buildings :")
self.buildingsLayout = QGridLayout() self.buildingsLayout = QGridLayout()
j = 0 j = 0
total_income = 0
received_income = 0
for i, building in enumerate(self.buildings): for i, building in enumerate(self.buildings):
if building.dcs_identifier not in FORTIFICATION_BUILDINGS: if building.dcs_identifier not in FORTIFICATION_BUILDINGS:
self.buildingsLayout.addWidget(QBuildingInfo(building, self.ground_object), j/3, j%3) self.buildingsLayout.addWidget(QBuildingInfo(building, self.ground_object), j/3, j%3)
j = j + 1 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.buildingBox.setLayout(self.buildingsLayout)
self.intelBox.setLayout(self.intelLayout) self.intelBox.setLayout(self.intelLayout)

View File

@ -10,15 +10,17 @@ from PySide2.QtWidgets import (
from dcs.planes import PlaneType from dcs.planes import PlaneType
from game import Game from game import Game
from game.theater import ControlPoint, OffMapSpawn
from gen.ato import Package from gen.ato import Package
from gen.flights.flight import Flight from gen.flights.flight import Flight
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector 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.QFlightTypeComboBox import QFlightTypeComboBox
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
from theater import ControlPoint
class QFlightCreator(QDialog): class QFlightCreator(QDialog):
@ -49,16 +51,30 @@ class QFlightCreator(QDialog):
self.on_aircraft_changed) self.on_aircraft_changed)
layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector)) layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector))
self.airfield_selector = QOriginAirfieldSelector( self.departure = QOriginAirfieldSelector(
self.game.aircraft_inventory, self.game.aircraft_inventory,
[cp for cp in game.theater.controlpoints if cp.captured], [cp for cp in game.theater.controlpoints if cp.captured],
self.aircraft_selector.currentData() self.aircraft_selector.currentData()
) )
self.airfield_selector.availability_changed.connect(self.update_max_size) self.departure.availability_changed.connect(self.update_max_size)
layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector)) 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.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)) layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
self.client_slots_spinner = QFlightSizeSpinner( self.client_slots_spinner = QFlightSizeSpinner(
@ -82,10 +98,16 @@ class QFlightCreator(QDialog):
def verify_form(self) -> Optional[str]: def verify_form(self) -> Optional[str]:
aircraft: PlaneType = self.aircraft_selector.currentData() 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() size: int = self.flight_size_spinner.value()
if not origin.captured: if not origin.captured:
return f"{origin.name} is not owned by your coalition." 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) available = origin.base.aircraft.get(aircraft, 0)
if not available: if not available:
return f"{origin.name} has no {aircraft.id} available." return f"{origin.name} has no {aircraft.id} available."
@ -104,14 +126,22 @@ class QFlightCreator(QDialog):
task = self.task_selector.currentData() task = self.task_selector.currentData()
aircraft = self.aircraft_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() 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" start_type = "Cold"
else: else:
start_type = "Warm" 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() flight.client_count = self.client_slots_spinner.value()
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@ -120,7 +150,9 @@ class QFlightCreator(QDialog):
def on_aircraft_changed(self, index: int) -> None: def on_aircraft_changed(self, index: int) -> None:
new_aircraft = self.aircraft_selector.itemData(index) 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: def update_max_size(self, available: int) -> None:
self.flight_size_spinner.setMaximum(min(available, 4)) self.flight_size_spinner.setMaximum(min(available, 4))

View File

@ -42,15 +42,7 @@ class QFlightWaypointList(QTableView):
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"]) self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
# The first waypoint is set up by pydcs at mission generation time, so waypoints = self.flight.flight_plan.waypoints
# 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)
for row, waypoint in enumerate(waypoints): for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, self.flight, waypoint) self.add_waypoint_row(row, self.flight, waypoint)
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)), self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),

View File

@ -12,7 +12,7 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QAbstractItemView, QListView from PySide2.QtWidgets import QAbstractItemView, QListView
import qt_ui.uiconstants as CONST import qt_ui.uiconstants as CONST
from theater import ConflictTheater from game.theater import ConflictTheater
@dataclass(frozen=True) @dataclass(frozen=True)
@ -29,14 +29,16 @@ class Campaign:
data = json.load(campaign_file) data = json.load(campaign_file)
sanitized_theater = data["theater"].replace(" ", "") sanitized_theater = data["theater"].replace(" ", "")
return cls(data["name"], f"Terrain_{sanitized_theater}", data.get("authors", "???"), return cls(data["name"], f"Terrain_{sanitized_theater}",
data.get("description", ""), ConflictTheater.from_json(data)) data.get("authors", "???"),
data.get("description", ""),
ConflictTheater.from_json(path.parent, data))
def load_campaigns() -> List[Campaign]: def load_campaigns() -> List[Campaign]:
campaign_dir = Path("resources\\campaigns") campaign_dir = Path("resources\\campaigns")
campaigns = [] campaigns = []
for path in campaign_dir.iterdir(): for path in campaign_dir.glob("*.json"):
try: try:
logging.debug(f"Loading campaign from {path}...") logging.debug(f"Loading campaign from {path}...")
campaign = Campaign.from_json(path) campaign = Campaign.from_json(path)

View File

@ -3,82 +3,5 @@
"theater": "Syria", "theater": "Syria",
"authors": "Khopa", "authors": "Khopa",
"description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>", "description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>",
"player_points": [ "miz": "inherent_resolve.miz"
{
"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"
]
]
} }

Binary file not shown.

View File

@ -1,2 +0,0 @@
# For save game compatibility. Remove before 2.3.
from game.theater import *

View File

@ -1,2 +0,0 @@
# For save compat. Remove in 2.3.
from game.theater.base import *

View File

@ -1,2 +0,0 @@
# For save compat. Remove in 2.3.
from game.theater.conflicttheater import *

View File

@ -1,2 +0,0 @@
# For save compat. Remove in 2.3.
from game.theater.controlpoint import *

View File

@ -1,3 +0,0 @@
# For save compat. Remove in 2.3.
from game.theater.frontline import *
from game.theater.conflicttheater import FrontLine

View File

@ -1,2 +0,0 @@
# For save compat. Remove in 2.3.
from game.theater.theatergroundobject import *