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
def unit_type_from_name(name: str) -> Optional[UnitType]:
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
if name in vehicle_map:
return vehicle_map[name]
elif name in plane_map:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,28 @@
from __future__ import annotations
import logging
import itertools
import json
import logging
from dataclasses import dataclass
from functools import cached_property
from itertools import tee
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
from dcs import Mission
from dcs.countries import (
CombinedJointTaskForcesBlue,
CombinedJointTaskForcesRed,
)
from dcs.country import Country
from dcs.mapping import Point
from dcs.planes import F_15C
from dcs.ships import (
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
USS_Arleigh_Burke_IIa,
)
from dcs.statics import Fortification
from dcs.terrain import (
caucasus,
nevada,
@ -16,11 +31,20 @@ from dcs.terrain import (
syria,
thechannel,
)
from dcs.terrain.terrain import Terrain
from dcs.terrain.terrain import Airport, Terrain
from dcs.unitgroup import (
FlyingGroup,
Group,
ShipGroup,
StaticGroup,
VehicleGroup,
)
from dcs.vehicles import AirDefence, Armor
from gen.flights.flight import FlightType
from .controlpoint import ControlPoint, MissionTarget
from .controlpoint import ControlPoint, MissionTarget, OffMapSpawn
from .landmap import Landmap, load_landmap, poly_contains
from ..utils import nm_to_meter
Numeric = Union[int, float]
@ -73,6 +97,266 @@ def pairwise(iterable):
return zip(a, b)
class MizCampaignLoader:
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
RED_COUNTRY = CombinedJointTaskForcesRed()
OFF_MAP_UNIT_TYPE = F_15C.id
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
LHA_UNIT_TYPE = LHA_1_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id
STRIKE_TARGET_UNIT_TYPE = Fortification.Workshop_A.id
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
# Multiple options for the required SAMs so campaign designers can more
# easily see the coverage of their IADS. Designers focused on campaigns that
# will primarily use SA-2s can place SA-2 launchers to ensure that they will
# have adequate coverage, and designers focused on campaigns that will
# primarily use SA-10s can do the same.
REQUIRED_SAM_UNIT_TYPES = {
AirDefence.SAM_Hawk_LN_M192,
AirDefence.SAM_Patriot_LN_M901,
AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
AirDefence.SAM_SA_10_S_300PS_LN_5P85D,
AirDefence.SAM_SA_2_LN_SM_90,
AirDefence.SAM_SA_3_S_125_LN_5P73,
}
BASE_DEFENSE_RADIUS = nm_to_meter(2)
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
self.theater = theater
self.mission = Mission()
self.mission.load_file(str(miz))
self.control_point_id = itertools.count(1000)
# If there are no red carriers there usually aren't red units. Make sure
# both countries are initialized so we don't have to deal with None.
if self.mission.country(self.BLUE_COUNTRY.name) is None:
self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY)
if self.mission.country(self.RED_COUNTRY.name) is None:
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
@staticmethod
def control_point_from_airport(airport: Airport) -> ControlPoint:
# TODO: Radials?
radials = LAND
# The wiki says this is a legacy property and to just use regular.
size = SIZE_REGULAR
# The importance is taken from the periodicity of the airport's
# warehouse divided by 10. 30 is the default, and out of range (valid
# values are between 1.0 and 1.4). If it is used, pick the default
# importance.
if airport.periodicity == 30:
importance = IMPORTANCE_MEDIUM
else:
importance = airport.periodicity / 10
cp = ControlPoint.from_airport(airport, radials, size, importance)
cp.captured = airport.is_blue()
# Use the unlimited aircraft option to determine if an airfield should
# be owned by the player when the campaign is "inverted".
cp.captured_invert = airport.unlimited_aircrafts
return cp
def country(self, blue: bool) -> Country:
country = self.mission.country(
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name)
# Should be guaranteed because we initialized them.
assert country
return country
@property
def blue(self) -> Country:
return self.country(blue=True)
@property
def red(self) -> Country:
return self.country(blue=False)
def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
for group in self.country(blue).plane_group:
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
yield group
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group:
if group.units[0].type == self.CV_UNIT_TYPE:
yield group
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group:
if group.units[0].type == self.LHA_UNIT_TYPE:
yield group
@property
def ships(self) -> Iterator[ShipGroup]:
for group in self.blue.ship_group:
if group.units[0].type == self.SHIP_UNIT_TYPE:
yield group
@property
def ewrs(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.EWR_UNIT_TYPE:
yield group
@property
def sams(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.SAM_UNIT_TYPE:
yield group
@property
def garrisons(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.GARRISON_UNIT_TYPE:
yield group
@property
def strike_targets(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
if group.units[0].type == self.STRIKE_TARGET_UNIT_TYPE:
yield group
@property
def offshore_strike_targets(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
yield group
@property
def required_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type == self.REQUIRED_SAM_UNIT_TYPES:
yield group
@cached_property
def control_points(self) -> Dict[int, ControlPoint]:
control_points = {}
for airport in self.mission.terrain.airport_list():
if airport.is_blue() or airport.is_red():
control_point = self.control_point_from_airport(airport)
control_points[control_point.id] = control_point
for blue in (False, True):
for group in self.off_map_spawns(blue):
control_point = OffMapSpawn(next(self.control_point_id),
str(group.name), group.position)
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point
for group in self.carriers(blue):
# TODO: Name the carrier.
control_point = ControlPoint.carrier(
"carrier", group.position, next(self.control_point_id))
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point
for group in self.lhas(blue):
# TODO: Name the LHA.
control_point = ControlPoint.lha(
"lha", group.position, next(self.control_point_id))
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point
return control_points
@property
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
for group in self.country(blue=True).vehicle_group:
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
yield group
@cached_property
def front_lines(self) -> Dict[str, ComplexFrontLine]:
# Dict of front line ID to a front line.
front_lines = {}
for group in self.front_line_path_groups:
# The unit will have its first waypoint at the source CP and the
# final waypoint at the destination CP. Intermediate waypoints
# define the curve of the front line.
waypoints = [p.position for p in group.points]
origin = self.mission.terrain.nearest_airport(waypoints[0])
if origin is None:
raise RuntimeError(
f"No airport near the first waypoint of {group.name}")
destination = self.mission.terrain.nearest_airport(waypoints[-1])
if destination is None:
raise RuntimeError(
f"No airport near the final waypoint of {group.name}")
# Snap the begin and end points to the control points.
waypoints[0] = origin.position
waypoints[-1] = destination.position
front_line_id = f"{origin.id}|{destination.id}"
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
self.control_points[origin.id].connect(
self.control_points[destination.id])
self.control_points[destination.id].connect(
self.control_points[origin.id])
return front_lines
def objective_info(self, group: Group) -> Tuple[ControlPoint, int]:
closest = self.theater.closest_control_point(group.position)
distance = closest.position.distance_to_point(group.position)
return closest, distance
def add_preset_locations(self) -> None:
for group in self.garrisons:
closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_garrisons.append(group.position)
else:
logging.warning(
f"Found garrison unit too far from base: {group.name}")
for group in self.sams:
closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_air_defense.append(group.position)
else:
closest.preset_locations.sams.append(group.position)
for group in self.ewrs:
closest, distance = self.objective_info(group)
closest.preset_locations.ewrs.append(group.position)
for group in self.strike_targets:
closest, distance = self.objective_info(group)
closest.preset_locations.strike_locations.append(group.position)
for group in self.offshore_strike_targets:
closest, distance = self.objective_info(group)
closest.preset_locations.offshore_strike_locations.append(
group.position)
for group in self.ships:
closest, distance = self.objective_info(group)
closest.preset_locations.ships.append(group.position)
for group in self.required_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.required_sams.append(group.position)
def populate_theater(self) -> None:
for control_point in self.control_points.values():
self.theater.add_controlpoint(control_point)
self.add_preset_locations()
self.theater.set_frontline_data(self.front_lines)
class ConflictTheater:
terrain: Terrain
@ -83,17 +367,35 @@ class ConflictTheater:
land_poly = None # type: Polygon
"""
daytime_map: Dict[str, Tuple[int, int]]
frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
_frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
def __init__(self):
self.controlpoints: List[ControlPoint] = []
self.frontline_data = FrontLine.load_json_frontlines(self)
self._frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
"""
self.land_poly = geometry.Polygon(self.landmap[0][0])
for x in self.landmap[1]:
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
"""
@property
def frontline_data(self) -> Optional[Dict[str, ComplexFrontLine]]:
if self._frontline_data is None:
self.load_frontline_data_from_file()
return self._frontline_data
def load_frontline_data_from_file(self) -> None:
if self._frontline_data is not None:
logging.warning("Replacing existing frontline data from file")
self._frontline_data = FrontLine.load_json_frontlines(self)
if self._frontline_data is None:
self._frontline_data = {}
def set_frontline_data(self, data: Dict[str, ComplexFrontLine]) -> None:
if self._frontline_data is not None:
logging.warning("Replacing existing frontline data")
self._frontline_data = data
def add_controlpoint(self, point: ControlPoint,
connected_to: Optional[List[ControlPoint]] = None):
if connected_to is None:
@ -153,11 +455,21 @@ class ConflictTheater:
def enemy_points(self) -> List[ControlPoint]:
return [point for point in self.controlpoints if not point.captured]
def closest_control_point(self, point: Point) -> ControlPoint:
closest = self.controlpoints[0]
closest_distance = point.distance_to_point(closest.position)
for control_point in self.controlpoints[1:]:
distance = point.distance_to_point(control_point.position)
if distance < closest_distance:
closest = control_point
closest_distance = distance
return closest
def add_json_cp(self, theater, p: dict) -> ControlPoint:
if p["type"] == "airbase":
airbase = theater.terrain.airports[p["id"]].__class__
airbase = theater.terrain.airports[p["id"]]
if "radials" in p.keys():
radials = p["radials"]
@ -188,7 +500,7 @@ class ConflictTheater:
return cp
@staticmethod
def from_json(data: Dict[str, Any]) -> ConflictTheater:
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
theaters = {
"Caucasus": CaucasusTheater,
"Nevada": NevadaTheater,
@ -199,6 +511,12 @@ class ConflictTheater:
}
theater = theaters[data["theater"]]
t = theater()
miz = data.get("miz", None)
if miz is not None:
MizCampaignLoader(directory / miz, t).populate_theater()
return t
cps = {}
for p in data["player_points"]:
cp = t.add_json_cp(theater, p)
@ -376,10 +694,6 @@ class FrontLine(MissionTarget):
"""Returns a tuple of the two control points."""
return self.control_point_a, self.control_point_b
@property
def middle_point(self):
self.point_from_a(self.attack_distance / 2)
@property
def attack_distance(self):
"""The total distance of all segments"""

View File

@ -1,9 +1,12 @@
from __future__ import annotations
import itertools
import logging
import random
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Iterator, List, TYPE_CHECKING
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
from dcs.mapping import Point
from dcs.ships import (
@ -13,6 +16,7 @@ from dcs.ships import (
Type_071_Amphibious_Transport_Dock,
)
from dcs.terrain.terrain import Airport
from dcs.unittype import FlyingType
from game import db
from gen.ground_forces.combat_stance import CombatStance
@ -20,12 +24,16 @@ from .base import Base
from .missiontarget import MissionTarget
from .theatergroundobject import (
BaseDefenseGroundObject,
EwrGroundObject,
SamGroundObject,
TheaterGroundObject,
VehicleGroupGroundObject,
)
if TYPE_CHECKING:
from game import Game
from gen.flights.flight import FlightType
from ..event import UnitsDeliveryEvent
class ControlPointType(Enum):
@ -34,6 +42,87 @@ class ControlPointType(Enum):
LHA_GROUP = 2 # A group with a Tarawa carrier (Helicopters & Harrier)
FARP = 4 # A FARP, with slots for helicopters
FOB = 5 # A FOB (ground units only)
OFF_MAP = 6
class LocationType(Enum):
BaseAirDefense = "base air defense"
Coastal = "coastal defense"
Ewr = "EWR"
Garrison = "garrison"
MissileSite = "missile site"
OffshoreStrikeTarget = "offshore strike target"
Sam = "SAM"
Ship = "ship"
Shorad = "SHORAD"
StrikeTarget = "strike target"
@dataclass
class PresetLocations:
"""Defines the preset locations loaded from the campaign mission file."""
#: Locations used for spawning ground defenses for bases.
base_garrisons: List[Point] = field(default_factory=list)
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
#: and SHORADs.
base_air_defense: List[Point] = field(default_factory=list)
#: Locations used by EWRs.
ewrs: List[Point] = field(default_factory=list)
#: Locations used by SAMs outside of bases.
sams: List[Point] = field(default_factory=list)
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
ships: List[Point] = field(default_factory=list)
#: Locations used by coastal defenses.
coastal_defenses: List[Point] = field(default_factory=list)
#: Locations used by ground based strike objectives.
strike_locations: List[Point] = field(default_factory=list)
#: Locations used by offshore strike objectives.
offshore_strike_locations: List[Point] = field(default_factory=list)
#: Locations of SAMs which should always be spawned.
required_sams: List[Point] = field(default_factory=list)
@staticmethod
def _random_from(points: List[Point]) -> Optional[Point]:
"""Finds, removes, and returns a random position from the given list."""
if not points:
return None
point = random.choice(points)
points.remove(point)
return point
def random_for(self, location_type: LocationType) -> Optional[Point]:
"""Returns a position suitable for the given location type.
The location, if found, will be claimed by the caller and not available
to subsequent calls.
"""
if location_type == LocationType.Garrison:
return self._random_from(self.base_garrisons)
if location_type == LocationType.Sam:
return self._random_from(self.sams)
if location_type == LocationType.BaseAirDefense:
return self._random_from(self.base_air_defense)
if location_type == LocationType.Ewr:
return self._random_from(self.ewrs)
if location_type == LocationType.Shorad:
return self._random_from(self.base_garrisons)
if location_type == LocationType.OffshoreStrikeTarget:
return self._random_from(self.offshore_strike_locations)
if location_type == LocationType.Ship:
return self._random_from(self.ships)
if location_type == LocationType.StrikeTarget:
return self._random_from(self.strike_locations)
logging.error(f"Unknown location type: {location_type}")
return None
class ControlPoint(MissionTarget):
@ -57,6 +146,7 @@ class ControlPoint(MissionTarget):
self.at = at
self.connected_objectives: List[TheaterGroundObject] = []
self.base_defenses: List[BaseDefenseGroundObject] = []
self.preset_locations = PresetLocations()
self.size = size
self.importance = importance
@ -69,6 +159,7 @@ class ControlPoint(MissionTarget):
self.cptype = cptype
self.stances: Dict[int, CombatStance] = {}
self.airport = None
self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None
@property
def ground_objects(self) -> List[TheaterGroundObject]:
@ -79,7 +170,7 @@ class ControlPoint(MissionTarget):
def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True):
assert airport
obj = cls(airport.id, airport.name, airport.position, airport, radials, size, importance, has_frontline, cptype=ControlPointType.AIRBASE)
obj.airport = airport()
obj.airport = airport
return obj
@classmethod
@ -144,7 +235,7 @@ class ControlPoint(MissionTarget):
return result
@property
def available_aircraft_slots(self):
def total_aircraft_parking(self):
"""
:return: The maximum number of aircraft that can be stored in this control point
"""
@ -157,7 +248,7 @@ class ControlPoint(MissionTarget):
else:
return 0
def connect(self, to):
def connect(self, to: ControlPoint) -> None:
self.connected_points.append(to)
self.stances[to.id] = CombatStance.DEFENSIVE
@ -222,6 +313,24 @@ class ControlPoint(MissionTarget):
def is_friendly(self, to_player: bool) -> bool:
return self.captured == to_player
def clear_base_defenses(self) -> None:
for base_defense in self.base_defenses:
if isinstance(base_defense, EwrGroundObject):
self.preset_locations.ewrs.append(base_defense.position)
elif isinstance(base_defense, SamGroundObject):
self.preset_locations.base_air_defense.append(
base_defense.position)
elif isinstance(base_defense, VehicleGroupGroundObject):
self.preset_locations.base_garrisons.append(
base_defense.position)
else:
logging.error(
"Could not determine preset location type for "
f"{base_defense}. Assuming garrison type.")
self.preset_locations.base_garrisons.append(
base_defense.position)
self.base_defenses = []
def capture(self, game: Game, for_player: bool) -> None:
if for_player:
self.captured = True
@ -233,9 +342,8 @@ class ControlPoint(MissionTarget):
self.base.aircraft = {}
self.base.armor = {}
# Handle cyclic dependency.
self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator
self.base_defenses = []
BaseDefenseGenerator(game, self).generate()
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
@ -260,3 +368,41 @@ class ControlPoint(MissionTarget):
yield from [
# TODO: FlightType.STRIKE
]
def can_land(self, aircraft: FlyingType) -> bool:
if self.is_carrier and aircraft not in db.CARRIER_CAPABLE:
return False
if self.is_lha and aircraft not in db.LHA_CAPABLE:
return False
return True
@property
def expected_aircraft_next_turn(self) -> int:
total = self.base.total_aircraft
assert self.pending_unit_deliveries
for unit_bought in self.pending_unit_deliveries.units:
if issubclass(unit_bought, FlyingType):
total += self.pending_unit_deliveries.units[unit_bought]
return total
@property
def unclaimed_parking(self) -> int:
return self.total_aircraft_parking - self.expected_aircraft_next_turn
class OffMapSpawn(ControlPoint):
def __init__(self, id: int, name: str, position: Point):
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
super().__init__(id, name, position, at=position, radials=[],
size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM,
has_frontline=False, cptype=ControlPointType.OFF_MAP)
def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Off map control points cannot be captured")
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from []
@property
def total_aircraft_parking(self) -> int:
return 1000

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 pickle
import random
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
@ -13,6 +13,18 @@ from dcs.vehicles import AirDefence
from game import Game, db
from game.factions.faction import Faction
from game.settings import Settings
from game.theater import LocationType
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
from game.theater.theatergroundobject import (
BuildingGroundObject,
CarrierGroundObject,
EwrGroundObject,
LhaGroundObject,
MissileSiteGroundObject,
SamGroundObject,
ShipGroundObject,
VehicleGroupGroundObject,
)
from game.version import VERSION
from gen import namegen
from gen.defenses.armor_group_generator import generate_armor_group
@ -21,29 +33,17 @@ from gen.fleet.ship_group_generator import (
generate_lha_group,
generate_ship_group,
)
from gen.locations.preset_location_finder import PresetLocationFinder
from gen.locations.preset_locations import PresetLocation
from gen.locations.preset_location_finder import MizDataLocationFinder
from gen.missiles.missiles_group_generator import generate_missile_group
from gen.sam.sam_group_generator import (
generate_anti_air_group,
generate_ewr_group, generate_shorad_group,
)
from theater import (
from . import (
ConflictTheater,
ControlPoint,
ControlPointType,
TheaterGroundObject,
)
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
from game.theater.theatergroundobject import (
EwrGroundObject,
SamGroundObject,
BuildingGroundObject,
CarrierGroundObject,
LhaGroundObject,
MissileSiteGroundObject,
ShipGroundObject,
VehicleGroupGroundObject,
OffMapSpawn,
)
GroundObjectTemplates = Dict[str, Dict[str, Any]]
@ -139,7 +139,13 @@ class GameGenerator:
control_point.base.commision_points = {}
control_point.base.strength = 1
# The tasks here are confusing. PinpointStrike for some reason means
# ground units.
for task in [PinpointStrike, CAP, CAS, AirDefence]:
if isinstance(control_point, OffMapSpawn):
# Off-map spawn locations start with no aircraft.
continue
if IMPORTANCE_HIGH <= control_point.importance <= IMPORTANCE_LOW:
raise ValueError(
f"CP importance must be between {IMPORTANCE_LOW} and "
@ -164,11 +170,155 @@ class GameGenerator:
control_point.base.commision_units({unit_type: count_per_type})
class LocationFinder:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
self.miz_data = MizDataLocationFinder.compute_possible_locations(
game.theater.terrain.name, control_point.full_name)
def location_for(self, location_type: LocationType) -> Optional[Point]:
position = self.control_point.preset_locations.random_for(location_type)
if position is not None:
return position
logging.warning(f"No campaign location for %s at %s",
location_type.value, self.control_point)
position = self.random_from_miz_data(
location_type == LocationType.OffshoreStrikeTarget)
if position is not None:
return position
logging.debug(f"No mizdata location for %s at %s", location_type.value,
self.control_point)
position = self.random_position(location_type)
if position is not None:
return position
logging.error(f"Could not find position for %s at %s",
location_type.value, self.control_point)
return None
def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
if offshore:
locations = self.miz_data.offshore_locations
else:
locations = self.miz_data.ashore_locations
if self.miz_data.offshore_locations:
preset = random.choice(locations)
locations.remove(preset)
return preset.position
return None
def random_position(self, location_type: LocationType) -> Optional[Point]:
# TODO: Flesh out preset locations so we never hit this case.
logging.warning("Falling back to random location for %s at %s",
location_type.value, self.control_point)
is_base_defense = location_type in {
LocationType.BaseAirDefense,
LocationType.Garrison,
LocationType.Shorad,
}
on_land = location_type not in {
LocationType.OffshoreStrikeTarget,
LocationType.Ship,
}
avoid_others = location_type not in {
LocationType.Garrison,
LocationType.MissileSite,
LocationType.Sam,
LocationType.Ship,
LocationType.Shorad,
}
if is_base_defense:
min_range = 400
max_range = 3200
elif location_type == LocationType.Ship:
min_range = 5000
max_range = 40000
elif location_type == LocationType.MissileSite:
min_range = 2500
max_range = 40000
else:
min_range = 10000
max_range = 40000
position = self._find_random_position(min_range, max_range,
on_land, is_base_defense,
avoid_others)
# Retry once, searching a bit further (On some big airbases, 3200 is too
# short (Ex : Incirlik)), but searching farther on every base would be
# problematic, as some base defense units would end up very far away
# from small airfields.
if position is None and is_base_defense:
position = self._find_random_position(3200, 4800,
on_land, is_base_defense,
avoid_others)
return position
def _find_random_position(self, min_range: int, max_range: int,
on_ground: bool, is_base_defense: bool,
avoid_others: bool) -> Optional[Point]:
"""
Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on
ground)
:param theater: Theater object
:param min_range: Minimal range from point
:param max_range: Max range from point
:param is_base_defense: True if the location is for base defense.
:return:
"""
near = self.control_point.position
others = self.control_point.ground_objects
def is_valid(point: Optional[Point]) -> bool:
if point is None:
return False
if on_ground and not self.game.theater.is_on_land(point):
return False
elif not on_ground and not self.game.theater.is_in_sea(point):
return False
if avoid_others:
for other in others:
if other.position.distance_to_point(point) < 10000:
return False
if is_base_defense:
# If it's a base defense we don't care how close it is to other
# points.
return True
# Else verify that it's not too close to another control point.
for control_point in self.game.theater.controlpoints:
if control_point != self.control_point:
if control_point.position.distance_to_point(point) < 30000:
return False
for ground_obj in control_point.ground_objects:
if ground_obj.position.distance_to_point(point) < 10000:
return False
return True
for _ in range(300):
# Check if on land or sea
p = near.random_point_within(max_range, min_range)
if is_valid(p):
return p
return None
class ControlPointGroundObjectGenerator:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
self.preset_locations = PresetLocationFinder.compute_possible_locations(game.theater.terrain.name, control_point.full_name)
self.location_finder = LocationFinder(game, control_point)
@property
def faction_name(self) -> str:
@ -205,11 +355,9 @@ class ControlPointGroundObjectGenerator:
self.generate_ship()
def generate_ship(self) -> None:
point = find_location(False, self.control_point.position,
self.game.theater, 5000, 40000, [], False)
point = self.location_finder.location_for(
LocationType.OffshoreStrikeTarget)
if point is None:
logging.error(
f"Could not find point for {self.control_point}'s navy")
return
group_id = self.game.next_group_id()
@ -223,26 +371,10 @@ class ControlPointGroundObjectGenerator:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
def pick_preset_location(self, offshore=False) -> Optional[PresetLocation]:
"""
Return a preset location if any is setup and still available for this point
@:param offshore Whether this should be an offshore location
@:return The preset location if found; None if it couldn't be found
"""
if offshore:
if len(self.preset_locations.offshore_locations) > 0:
location = random.choice(self.preset_locations.offshore_locations)
self.preset_locations.offshore_locations.remove(location)
logging.info("Picked a preset offshore location")
return location
else:
if len(self.preset_locations.ashore_locations) > 0:
location = random.choice(self.preset_locations.ashore_locations)
self.preset_locations.ashore_locations.remove(location)
logging.info("Picked a preset ashore location")
return location
logging.info("No preset location found")
return None
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate(self) -> bool:
return True
class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
@ -299,6 +431,7 @@ class BaseDefenseGenerator:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
self.location_finder = LocationFinder(game, control_point)
@property
def faction_name(self) -> str:
@ -317,10 +450,8 @@ class BaseDefenseGenerator:
self.generate_base_defenses()
def generate_ewr(self) -> None:
position = self._find_location()
position = self.location_finder.location_for(LocationType.Ewr)
if position is None:
logging.error("Could not find position for "
f"{self.control_point} EWR")
return
group_id = self.game.next_group_id()
@ -350,10 +481,8 @@ class BaseDefenseGenerator:
self.generate_garrison()
def generate_garrison(self) -> None:
position = self._find_location()
position = self.location_finder.location_for(LocationType.Garrison)
if position is None:
logging.error("Could not find position for "
f"{self.control_point} garrison")
return
group_id = self.game.next_group_id()
@ -368,10 +497,9 @@ class BaseDefenseGenerator:
self.control_point.base_defenses.append(g)
def generate_sam(self) -> None:
position = self._find_location()
position = self.location_finder.location_for(
LocationType.BaseAirDefense)
if position is None:
logging.error("Could not find position for "
f"{self.control_point} SAM")
return
group_id = self.game.next_group_id()
@ -385,10 +513,9 @@ class BaseDefenseGenerator:
self.control_point.base_defenses.append(g)
def generate_shorad(self) -> None:
position = self._find_location()
position = self.location_finder.location_for(
LocationType.BaseAirDefense)
if position is None:
logging.error("Could not find position for "
f"{self.control_point} SHORAD")
return
group_id = self.game.next_group_id()
@ -401,20 +528,6 @@ class BaseDefenseGenerator:
g.groups.append(group)
self.control_point.base_defenses.append(g)
def _find_location(self) -> Optional[Point]:
position = find_location(True, self.control_point.position,
self.game.theater, 400, 3200, [], True)
# Retry once, searching a bit further (On some big airbase, 3200 is too short (Ex : Incirlik))
# But searching farther on every base would be problematic, as some base defense units
# would end up very far away from small airfields.
# (I know it's not good for performance, but this is only done on campaign generation)
# TODO : Make the whole process less stupid with preset possible positions for each airbase
if position is None:
position = find_location(True, self.control_point.position,
self.game.theater, 3200, 4800, [], True)
return position
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__(self, game: Game, control_point: ControlPoint,
@ -442,15 +555,31 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
# Always generate at least one AA point.
self.generate_aa_site()
skip_sams = self.generate_required_aa()
# And between 2 and 7 other objectives.
amount = random.randrange(2, 7)
for i in range(amount):
# 1 in 4 additional objectives are AA.
if random.randint(0, 3) == 0:
self.generate_aa_site()
if skip_sams > 0:
skip_sams -= 1
else:
self.generate_aa_site()
else:
self.generate_ground_point()
def generate_required_aa(self) -> int:
"""Generates the AA sites that are required by the campaign.
Returns:
The number of AA sites that were generated.
"""
sams = self.control_point.preset_locations.required_sams
for position in sams:
self.generate_aa_at(position)
return len(sams)
def generate_ground_point(self) -> None:
try:
category = random.choice(self.faction.building_set)
@ -461,23 +590,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
offshore = category == "oil"
if category == "oil":
location_type = LocationType.OffshoreStrikeTarget
else:
location_type = LocationType.StrikeTarget
# Pick from preset locations
location = self.pick_preset_location(offshore)
# Else try the old algorithm
if location is None:
point = find_location(not offshore,
self.control_point.position,
self.game.theater, 10000, 40000,
self.control_point.ground_objects)
else:
point = location.position
point = self.location_finder.location_for(location_type)
if point is None:
logging.error(
f"Could not find point for {obj_name} at {self.control_point}")
return
object_id = 0
@ -495,24 +615,12 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
def generate_aa_site(self) -> None:
obj_name = namegen.random_objective_name()
# Pick from preset locations
location = self.pick_preset_location(False)
# If no preset location, then try the old algorithm
if location is None:
position = find_location(True, self.control_point.position,
self.game.theater, 10000, 40000,
self.control_point.ground_objects)
else:
position = location.position
position = self.location_finder.location_for(LocationType.Sam)
if position is None:
logging.error(
f"Could not find point for {obj_name} at {self.control_point}")
return
self.generate_aa_at(position)
def generate_aa_at(self, position: Point) -> None:
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
@ -527,22 +635,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.generate_missile_site()
def generate_missile_site(self) -> None:
# Pick from preset locations
location = self.pick_preset_location(False)
# If no preset location, then try the old algorithm
if location is None:
position = find_location(True, self.control_point.position,
self.game.theater, 2500, 40000,
[], False)
else:
position = location.position
position = self.location_finder.location_for(LocationType.MissileSite)
if position is None:
logging.info(
f"Could not find point for {self.control_point} missile site")
return
group_id = self.game.next_group_id()
@ -577,72 +671,9 @@ class GroundObjectGenerator:
generator = CarrierGroundObjectGenerator(self.game, control_point)
elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator(self.game, control_point)
elif isinstance(control_point, OffMapSpawn):
generator = NoOpGroundObjectGenerator(self.game, control_point)
else:
generator = AirbaseGroundObjectGenerator(self.game, control_point,
self.templates)
return generator.generate()
# TODO: https://stackoverflow.com/a/19482012/632035
# A lot of the time spent on mission generation is spent in this function since
# just randomly guess up to 1800 times and often fail. This is particularly
# problematic while trying to find placement for navies in Nevada.
def find_location(on_ground: bool, near: Point, theater: ConflictTheater,
min_range: int, max_range: int,
others: List[TheaterGroundObject],
is_base_defense: bool = False) -> Optional[Point]:
"""
Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on
ground)
:param near: Point
:param theater: Theater object
:param min_range: Minimal range from point
:param max_range: Max range from point
:param others: Other already existing ground objects
:param is_base_defense: True if the location is for base defense.
:return:
"""
point = None
for _ in range(300):
# Check if on land or sea
p = near.random_point_within(max_range, min_range)
if on_ground and theater.is_on_land(p):
point = p
elif not on_ground and theater.is_in_sea(p):
point = p
if point:
for angle in range(0, 360, 45):
p = point.point_from_heading(angle, 2500)
if on_ground and not theater.is_on_land(p):
point = None
break
elif not on_ground and not theater.is_in_sea(p):
point = None
break
if point:
for other in others:
if other.position.distance_to_point(point) < 10000:
point = None
break
if point:
for control_point in theater.controlpoints:
if is_base_defense:
break
if control_point.position != near:
if point is None:
break
if control_point.position.distance_to_point(point) < 30000:
point = None
break
for ground_obj in control_point.ground_objects:
if ground_obj.position.distance_to_point(point) < 10000:
point = None
break
if point:
return point
return None

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,8 @@ from typing import Tuple
from dcs.country import Country
from dcs.mapping import Point
from theater import ConflictTheater, ControlPoint, FrontLine
from game.theater.conflicttheater import ConflictTheater, FrontLine
from game.theater.controlpoint import ControlPoint
AIR_DISTANCE = 40000

View File

@ -16,11 +16,24 @@ from typing import (
Type,
)
from dcs.unittype import FlyingType, UnitType
from dcs.unittype import FlyingType
from game import db
from game.data.radar_db import UNITS_WITH_RADAR
from game.infos.information import Information
from game.theater import (
ControlPoint,
FrontLine,
MissionTarget,
OffMapSpawn,
SamGroundObject,
TheaterGroundObject,
)
# Avoid importing some types that cause circular imports unless type checking.
from game.theater.theatergroundobject import (
EwrGroundObject,
NavalGroundObject, VehicleGroupGroundObject,
)
from game.utils import nm_to_meter
from gen import Conflict
from gen.ato import Package
@ -46,19 +59,6 @@ from gen.flights.flight import (
)
from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator
from theater import (
ControlPoint,
FrontLine,
MissionTarget,
TheaterGroundObject,
SamGroundObject,
)
# Avoid importing some types that cause circular imports unless type checking.
from game.theater.theatergroundobject import (
EwrGroundObject,
NavalGroundObject, VehicleGroupGroundObject,
)
if TYPE_CHECKING:
from game import Game
@ -119,7 +119,7 @@ class AircraftAllocator:
def find_aircraft_for_flight(
self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, UnitType]]:
) -> Optional[Tuple[ControlPoint, FlyingType]]:
"""Finds aircraft suitable for the given mission.
Searches for aircraft capable of performing the given mission within the
@ -190,7 +190,7 @@ class AircraftAllocator:
def find_aircraft_of_type(
self, flight: ProposedFlight, types: List[Type[FlyingType]],
) -> Optional[Tuple[ControlPoint, UnitType]]:
) -> Optional[Tuple[ControlPoint, FlyingType]]:
airfields_in_range = self.closest_airfields.airfields_within(
flight.max_distance
)
@ -214,6 +214,8 @@ class PackageBuilder:
global_inventory: GlobalAircraftInventory,
is_player: bool,
start_type: str) -> None:
self.closest_airfields = closest_airfields
self.is_player = is_player
self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
is_player)
@ -232,11 +234,32 @@ class PackageBuilder:
if assignment is None:
return False
airfield, aircraft = assignment
flight = Flight(self.package, aircraft, plan.num_aircraft, airfield,
plan.task, self.start_type)
if isinstance(airfield, OffMapSpawn):
start_type = "In Flight"
else:
start_type = self.start_type
flight = Flight(self.package, aircraft, plan.num_aircraft, plan.task,
start_type, departure=airfield, arrival=airfield,
divert=self.find_divert_field(aircraft, airfield))
self.package.add_flight(flight)
return True
def find_divert_field(self, aircraft: FlyingType,
arrival: ControlPoint) -> Optional[ControlPoint]:
divert_limit = nm_to_meter(150)
for airfield in self.closest_airfields.airfields_within(divert_limit):
if airfield.captured != self.is_player:
continue
if airfield == arrival:
continue
if not airfield.can_land(aircraft):
continue
if isinstance(airfield, OffMapSpawn):
continue
return airfield
return None
def build(self) -> Package:
"""Returns the built package."""
return self.package
@ -406,6 +429,9 @@ class ObjectiveFinder:
CP.
"""
for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn):
# Off-map spawn locations don't need protection.
continue
airfields_in_proximity = self.closest_airfields_to(cp)
airfields_in_threat_range = airfields_in_proximity.airfields_within(
self.AIRFIELD_THREAT_RANGE

View File

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

View File

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

View File

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

View File

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

View File

@ -8,11 +8,14 @@ from dcs.unit import Unit
from dcs.unitgroup import VehicleGroup
from game.data.doctrine import Doctrine
from game.utils import nm_to_meter
from game.theater import (
ControlPoint,
MissionTarget,
OffMapSpawn,
TheaterGroundObject,
)
from game.weather import Conditions
from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType
from ..runways import RunwayAssigner
@dataclass(frozen=True)
@ -34,8 +37,7 @@ class WaypointBuilder:
def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False)
@staticmethod
def takeoff(departure: ControlPoint) -> FlightWaypoint:
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
Note that the takeoff waypoint will automatically be created by pydcs
@ -46,36 +48,93 @@ class WaypointBuilder:
departure: Departure airfield or carrier.
"""
position = departure.position
waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF,
position.x,
position.y,
0
)
waypoint.name = "TAKEOFF"
waypoint.alt_type = "RADIO"
waypoint.description = "Takeoff"
waypoint.pretty_name = "Takeoff"
if isinstance(departure, OffMapSpawn):
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
position.x,
position.y,
500 if self.is_helo else self.doctrine.rendezvous_altitude
)
waypoint.name = "NAV"
waypoint.alt_type = "BARO"
waypoint.description = "Enter theater"
waypoint.pretty_name = "Enter theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF,
position.x,
position.y,
0
)
waypoint.name = "TAKEOFF"
waypoint.alt_type = "RADIO"
waypoint.description = "Takeoff"
waypoint.pretty_name = "Takeoff"
return waypoint
@staticmethod
def land(arrival: ControlPoint) -> FlightWaypoint:
def land(self, arrival: ControlPoint) -> FlightWaypoint:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
arrival: Arrival airfield or carrier.
"""
position = arrival.position
if isinstance(arrival, OffMapSpawn):
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
position.x,
position.y,
500 if self.is_helo else self.doctrine.rendezvous_altitude
)
waypoint.name = "NAV"
waypoint.alt_type = "BARO"
waypoint.description = "Exit theater"
waypoint.pretty_name = "Exit theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
position.x,
position.y,
0
)
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
waypoint.description = "Land"
waypoint.pretty_name = "Land"
return waypoint
def divert(self,
divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
"""Create divert waypoint for the given arrival airfield or carrier.
Args:
divert: Divert airfield or carrier.
"""
if divert is None:
return None
position = divert.position
if isinstance(divert, OffMapSpawn):
if self.is_helo:
altitude = 500
else:
altitude = self.doctrine.rendezvous_altitude
altitude_type = "BARO"
else:
altitude = 0
altitude_type = "RADIO"
waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
FlightWaypointType.DIVERT,
position.x,
position.y,
0
altitude
)
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
waypoint.description = "Land"
waypoint.pretty_name = "Land"
waypoint.alt_type = altitude_type
waypoint.name = "DIVERT"
waypoint.description = "Divert"
waypoint.pretty_name = "Divert"
waypoint.only_for_player = True
return waypoint
def hold(self, position: Point) -> FlightWaypoint:

View File

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

View File

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

View File

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

View File

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

View File

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

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 dcs.planes import PlaneType
from dcs.unittype import FlyingType
class QAircraftTypeSelector(QComboBox):
"""Combo box for selecting among the given aircraft types."""
def __init__(self, aircraft_types: Iterable[PlaneType]) -> None:
def __init__(self, aircraft_types: Iterable[FlyingType]) -> None:
super().__init__()
for aircraft in aircraft_types:
self.addItem(f"{aircraft.id}", userData=aircraft)

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 theater import ConflictTheater, MissionTarget
from game.theater import ConflictTheater, MissionTarget
class QFlightTypeComboBox(QComboBox):

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import datetime
import logging
import math
from typing import Iterable, List, Optional, Tuple, Iterator
from typing import Iterable, Iterator, List, Optional, Tuple
from PySide2.QtCore import QPointF, Qt
from PySide2.QtGui import (
@ -27,6 +27,13 @@ from dcs.mapping import point_from_heading
import qt_ui.uiconstants as CONST
from game import Game, db
from game.theater import ControlPoint
from game.theater.conflicttheater import FrontLine
from game.theater.theatergroundobject import (
EwrGroundObject,
MissileSiteGroundObject,
TheaterGroundObject,
)
from game.utils import meter_to_feet
from game.weather import TimeOfDay
from gen import Conflict
@ -39,13 +46,7 @@ from qt_ui.widgets.map.QLiberationScene import QLiberationScene
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from theater import ControlPoint
from game.theater.conflicttheater import FrontLine
from game.theater.theatergroundobject import (
EwrGroundObject,
MissileSiteGroundObject,
TheaterGroundObject,
)
def binomial(i: int, n: int) -> float:
"""Binomial coefficient"""
@ -373,6 +374,10 @@ class QLiberationMap(QGraphicsView):
FlightWaypointType.TARGET_SHIP,
)
for idx, point in enumerate(flight.flight_plan.waypoints[1:]):
if point.waypoint_type == FlightWaypointType.DIVERT:
# Don't clutter the map showing divert points.
continue
new_pos = self._transform_point(Point(point.x, point.y))
self.draw_flight_path(scene, prev_pos, new_pos, is_player,
selected)
@ -386,7 +391,6 @@ class QLiberationMap(QGraphicsView):
self.draw_waypoint_info(scene, idx + 1, point, new_pos,
flight.flight_plan)
prev_pos = tuple(new_pos)
self.draw_flight_path(scene, prev_pos, pos, is_player, selected)
def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
player: bool, selected: bool) -> None:

View File

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

View File

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

View File

@ -47,9 +47,12 @@ class QMapObject(QGraphicsRectItem):
object_details_action.triggered.connect(self.on_click)
menu.addAction(object_details_action)
new_package_action = QAction(f"New package")
new_package_action.triggered.connect(self.open_new_package_dialog)
menu.addAction(new_package_action)
# Not all locations have valid objetives. Off-map spawns, for example,
# have no mission types.
if list(self.mission_target.mission_types(for_player=True)):
new_package_action = QAction(f"New package")
new_package_action.triggered.connect(self.open_new_package_dialog)
menu.addAction(new_package_action)
self.add_context_menu_actions(menu)

View File

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

View File

@ -1,43 +1,34 @@
from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QTabWidget
from PySide2.QtWidgets import QTabWidget
from game.theater import ControlPoint, OffMapSpawn
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand
from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ
from qt_ui.windows.basemenu.ground_forces.QGroundForcesHQ import QGroundForcesHQ
from qt_ui.windows.basemenu.intel.QIntelInfo import QIntelInfo
from theater import ControlPoint
class QBaseMenuTabs(QTabWidget):
def __init__(self, cp: ControlPoint, game_model: GameModel):
super(QBaseMenuTabs, self).__init__()
self.cp = cp
if cp:
if not cp.captured:
if not cp.is_carrier:
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Base Defenses")
self.intel = QIntelInfo(cp, game_model.game)
self.addTab(self.intel, "Intel")
else:
if cp.has_runway():
self.airfield_command = QAirfieldCommand(cp, game_model)
self.addTab(self.airfield_command, "Airfield Command")
if not cp.is_carrier:
self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
self.addTab(self.ground_forces_hq, "Ground Forces HQ")
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Base Defenses")
else:
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Fleet")
if not cp.captured:
if not cp.is_carrier and not isinstance(cp, OffMapSpawn):
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Base Defenses")
self.intel = QIntelInfo(cp, game_model.game)
self.addTab(self.intel, "Intel")
else:
tabError = QFrame()
l = QGridLayout()
l.addWidget(QLabel("No Control Point"))
tabError.setLayout(l)
self.addTab(tabError, "No Control Point")
if cp.has_runway():
self.airfield_command = QAirfieldCommand(cp, game_model)
self.addTab(self.airfield_command, "Airfield Command")
if cp.is_carrier:
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Fleet")
elif not isinstance(cp, OffMapSpawn):
self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
self.addTab(self.ground_forces_hq, "Ground Forces HQ")
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Base Defenses")

View File

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

View File

@ -1,3 +1,4 @@
import logging
from typing import Optional, Set
from PySide2.QtCore import Qt
@ -11,13 +12,14 @@ from PySide2.QtWidgets import (
QVBoxLayout,
QWidget,
)
from dcs.task import CAP, CAS
from dcs.unittype import UnitType
from game.event.event import UnitsDeliveryEvent
from game import db
from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.uiconstants import ICONS
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
from theater import CAP, CAS, ControlPoint, db
class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
@ -25,25 +27,18 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
QFrame.__init__(self)
self.cp = cp
self.game_model = game_model
self.deliveryEvent: Optional[UnitsDeliveryEvent] = None
self.bought_amount_labels = {}
self.existing_units_labels = {}
for event in self.game_model.game.events:
if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp:
self.deliveryEvent = event
if not self.deliveryEvent:
self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp)
# Determine maximum number of aircrafts that can be bought
self.set_maximum_units(self.cp.available_aircraft_slots)
self.set_maximum_units(self.cp.total_aircraft_parking)
self.set_recruitable_types([CAP, CAS])
self.bought_amount_labels = {}
self.existing_units_labels = {}
self.hangar_status = QHangarStatus(self.total_units, self.cp.available_aircraft_slots)
self.hangar_status = QHangarStatus(self.cp)
self.init_ui()
@ -86,13 +81,18 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
self.setLayout(main_layout)
def buy(self, unit_type):
if self.maximum_units > 0:
if self.cp.unclaimed_parking <= 0:
logging.debug(f"No space for additional aircraft at {self.cp}.")
return
super().buy(unit_type)
self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
self.hangar_status.update_label()
def sell(self, unit_type: UnitType):
# Don't need to remove aircraft from the inventory if we're canceling
# orders.
if self.deliveryEvent.units.get(unit_type, 0) <= 0:
if self.pending_deliveries.units.get(unit_type, 0) <= 0:
global_inventory = self.game_model.game.aircraft_inventory
inventory = global_inventory.for_control_point(self.cp)
try:
@ -105,22 +105,26 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
"assigned to a mission?", QMessageBox.Ok)
return
super().sell(unit_type)
self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
self.hangar_status.update_label()
class QHangarStatus(QHBoxLayout):
def __init__(self, current_amount: int, max_amount: int):
super(QHangarStatus, self).__init__()
def __init__(self, control_point: ControlPoint) -> None:
super().__init__()
self.control_point = control_point
self.icon = QLabel()
self.icon.setPixmap(ICONS["Hangar"])
self.text = QLabel("")
self.update_label(current_amount, max_amount)
self.update_label()
self.addWidget(self.icon, Qt.AlignLeft)
self.addWidget(self.text, Qt.AlignLeft)
self.addStretch(50)
self.setAlignment(Qt.AlignLeft)
def update_label(self, current_amount: int, max_amount: int):
self.text.setText("<strong>{}/{}</strong>".format(current_amount, max_amount))
def update_label(self) -> None:
current_amount = self.control_point.expected_aircraft_next_turn
max_amount = self.control_point.total_aircraft_parking
self.text.setText(f"<strong>{current_amount}/{max_amount}</strong>")

View File

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

View File

@ -1,10 +1,16 @@
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout
from PySide2.QtWidgets import (
QGridLayout,
QGroupBox,
QLabel,
QPushButton,
QVBoxLayout,
)
from game.theater import ControlPoint, TheaterGroundObject
from qt_ui.dialogs import Dialog
from qt_ui.uiconstants import VEHICLES_ICONS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from theater import ControlPoint, TheaterGroundObject
class QBaseDefenseGroupInfo(QGroupBox):

View File

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

View File

@ -1,10 +1,15 @@
from PySide2.QtGui import Qt
from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QVBoxLayout, QFrame, QWidget, QScrollArea
from PySide2.QtWidgets import (
QFrame,
QGridLayout,
QScrollArea,
QVBoxLayout,
QWidget,
)
from game import db
from qt_ui.uiconstants import AIRCRAFT_ICONS, VEHICLES_ICONS
from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import QBaseDefenseGroupInfo
from theater import ControlPoint, Airport
from game.theater import Airport, ControlPoint
from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import \
QBaseDefenseGroupInfo
class QBaseInformation(QFrame):

View File

@ -6,11 +6,12 @@ from PySide2.QtWidgets import (
QVBoxLayout,
QWidget,
)
from dcs.task import PinpointStrike
from game.event import UnitsDeliveryEvent
from game import db
from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
from theater import ControlPoint, PinpointStrike, db
class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
@ -23,12 +24,6 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
self.bought_amount_labels = {}
self.existing_units_labels = {}
for event in self.game_model.game.events:
if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp:
self.deliveryEvent = event
if not self.deliveryEvent:
self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp)
self.init_ui()
def init_ui(self):
@ -61,4 +56,4 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
scroll.setWidgetResizable(True)
scroll.setWidget(scroll_content)
main_layout.addWidget(scroll)
self.setLayout(main_layout)
self.setLayout(main_layout)

View File

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

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 qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import QGroundForcesStrategySelector
from theater import ControlPoint
from game.theater import ControlPoint
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import \
QGroundForcesStrategySelector
class QGroundForcesStrategy(QGroupBox):

View File

@ -1,6 +1,6 @@
from PySide2.QtWidgets import QComboBox
from theater import ControlPoint, CombatStance
from game.theater import CombatStance, ControlPoint
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 PySide2.QtWidgets import QLabel, QGroupBox, QVBoxLayout, QFrame, QGridLayout
from dcs.task import Embarking, CAS, PinpointStrike, CAP
from game import Game
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import QGroundForcesStrategySelector
from theater import ControlPoint, db
from game import Game, db
from game.theater import ControlPoint
class QIntelInfo(QFrame):

View File

@ -2,7 +2,7 @@ import os
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QVBoxLayout, QLabel
from game.db import REWARDS
class QBuildingInfo(QGroupBox):
@ -28,6 +28,13 @@ class QBuildingInfo(QGroupBox):
layout = QVBoxLayout()
layout.addWidget(self.header)
layout.addWidget(self.name)
if self.building.category in REWARDS.keys():
income_label_text = 'Value: ' + str(REWARDS[self.building.category]) + "M"
if self.building.is_dead:
income_label_text = '<s>' + income_label_text + '</s>'
self.reward = QLabel(income_label_text)
layout.addWidget(self.reward)
footer = QHBoxLayout()
self.setLayout(layout)

View File

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

View File

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

View File

@ -42,15 +42,7 @@ class QFlightWaypointList(QTableView):
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
# The first waypoint is set up by pydcs at mission generation time, so
# we need to add that waypoint manually.
takeoff = FlightWaypoint(self.flight.from_cp.position.x,
self.flight.from_cp.position.y, 0)
takeoff.description = "Take Off"
takeoff.name = takeoff.pretty_name = "Take Off from " + self.flight.from_cp.name
takeoff.alt_type = "RADIO"
waypoints = itertools.chain([takeoff], self.flight.points)
waypoints = self.flight.flight_plan.waypoints
for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, self.flight, waypoint)
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),

View File

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

View File

@ -3,82 +3,5 @@
"theater": "Syria",
"authors": "Khopa",
"description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>",
"player_points": [
{
"type": "airbase",
"id": "King Hussein Air College",
"size": 1000,
"importance": 1.4
},
{
"type": "airbase",
"id": "Incirlik",
"size": 1000,
"importance": 1.4,
"captured_invert": true
},
{
"type": "carrier",
"id": 1001,
"x": -210000,
"y": -200000,
"captured_invert": true
},
{
"type": "lha",
"id": 1002,
"x": -131000,
"y": -161000,
"captured_invert": true
}
],
"enemy_points": [
{
"type": "airbase",
"id": "Khalkhalah",
"size": 1000,
"importance": 1.2
},
{
"type": "airbase",
"id": "Palmyra",
"size": 1000,
"importance": 1
},
{
"type": "airbase",
"id": "Tabqa",
"size": 1000,
"importance": 1
},
{
"type": "airbase",
"id": "Jirah",
"size": 1000,
"importance": 1,
"captured_invert": true
}
],
"links": [
[
"Khalkhalah",
"King Hussein Air College"
],
[
"Incirlik",
"Incirlik"
],
[
"Khalkhalah",
"Palmyra"
],
[
"Palmyra",
"Tabqa"
],
[
"Jirah",
"Tabqa"
]
]
"miz": "inherent_resolve.miz"
}

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 *