mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop
This commit is contained in:
commit
799b0fae94
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
25
game/game.py
25
game/game.py
@ -26,7 +26,7 @@ from .event.frontlineattack import FrontlineAttackEvent
|
||||
from .factions.faction import Faction
|
||||
from .infos.information import Information
|
||||
from .settings import Settings
|
||||
from .theater import ConflictTheater, ControlPoint
|
||||
from .theater import ConflictTheater, ControlPoint, OffMapSpawn
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
COMMISION_UNIT_VARIETY = 4
|
||||
@ -151,7 +151,7 @@ class Game:
|
||||
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
|
||||
for cp in self.theater.player_points():
|
||||
for g in cp.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
reward = reward + REWARDS[g.category]
|
||||
return reward
|
||||
else:
|
||||
@ -160,9 +160,6 @@ class Game:
|
||||
def _budget_player(self):
|
||||
self.budget += self.budget_reward_amount
|
||||
|
||||
def awacs_expense_commit(self):
|
||||
self.budget -= AWACS_BUDGET_COST
|
||||
|
||||
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
|
||||
event = UnitsDeliveryEvent(attacker_name=self.player_name,
|
||||
defender_name=self.player_name,
|
||||
@ -172,10 +169,6 @@ class Game:
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
def units_delivery_remove(self, event: Event):
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
|
||||
def initiate_event(self, event: Event):
|
||||
#assert event in self.events
|
||||
logging.info("Generating {} (regular)".format(event))
|
||||
@ -202,12 +195,6 @@ class Game:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
|
||||
# Save game compatibility.
|
||||
|
||||
# TODO: Remove in 2.3.
|
||||
if not hasattr(self, "conditions"):
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
||||
@ -248,6 +235,7 @@ class Game:
|
||||
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(cp)
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
@ -274,7 +262,7 @@ class Game:
|
||||
production = 0.0
|
||||
for enemy_point in self.theater.enemy_points():
|
||||
for g in enemy_point.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
production = production + REWARDS[g.category]
|
||||
|
||||
production = production * 0.75
|
||||
@ -289,6 +277,9 @@ class Game:
|
||||
if len(potential_cp_armor) == 0:
|
||||
potential_cp_armor = self.theater.enemy_points()
|
||||
|
||||
potential_cp_armor = [p for p in potential_cp_armor if
|
||||
not isinstance(p, OffMapSpawn)]
|
||||
|
||||
i = 0
|
||||
potential_units = db.FACTIONS[self.enemy_name].frontline_units
|
||||
|
||||
@ -325,7 +316,7 @@ class Game:
|
||||
if i > 50 or budget_for_aircraft <= 0:
|
||||
break
|
||||
target_cp = random.choice(potential_cp_armor)
|
||||
if target_cp.base.total_planes >= MAX_AIRCRAFT:
|
||||
if target_cp.base.total_aircraft >= MAX_AIRCRAFT:
|
||||
continue
|
||||
unit = random.choice(potential_units)
|
||||
price = db.PRICES[unit] * 2
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from theater import ControlPoint
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
class FrontlineData:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1 +0,0 @@
|
||||
"""Only here to keep compatibility for save games generated in version 2.2.0"""
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:]
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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...
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
40
qt_ui/widgets/combos/QArrivalAirfieldSelector.py
Normal file
40
qt_ui/widgets/combos/QArrivalAirfieldSelector.py
Normal 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()
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
from PySide2.QtWidgets import QComboBox
|
||||
|
||||
from theater import ConflictTheater, MissionTarget
|
||||
from game.theater import ConflictTheater, MissionTarget
|
||||
|
||||
|
||||
class QFlightTypeComboBox(QComboBox):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
@ -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
|
||||
|
||||
@ -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>")
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from PySide2.QtWidgets import QComboBox
|
||||
|
||||
from theater import ControlPoint, CombatStance
|
||||
from game.theater import CombatStance, ControlPoint
|
||||
|
||||
|
||||
class QGroundForcesStrategySelector(QComboBox):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
}
|
||||
BIN
resources/campaigns/inherent_resolve.miz
Normal file
BIN
resources/campaigns/inherent_resolve.miz
Normal file
Binary file not shown.
@ -1,2 +0,0 @@
|
||||
# For save game compatibility. Remove before 2.3.
|
||||
from game.theater import *
|
||||
@ -1,2 +0,0 @@
|
||||
# For save compat. Remove in 2.3.
|
||||
from game.theater.base import *
|
||||
@ -1,2 +0,0 @@
|
||||
# For save compat. Remove in 2.3.
|
||||
from game.theater.conflicttheater import *
|
||||
@ -1,2 +0,0 @@
|
||||
# For save compat. Remove in 2.3.
|
||||
from game.theater.controlpoint import *
|
||||
@ -1,3 +0,0 @@
|
||||
# For save compat. Remove in 2.3.
|
||||
from game.theater.frontline import *
|
||||
from game.theater.conflicttheater import FrontLine
|
||||
@ -1,2 +0,0 @@
|
||||
# For save compat. Remove in 2.3.
|
||||
from game.theater.theatergroundobject import *
|
||||
Loading…
x
Reference in New Issue
Block a user