mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Move theater into game.
This commit is contained in:
parent
482bedd739
commit
8345063e84
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -36,11 +36,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
./venv/scripts/activate
|
./venv/scripts/activate
|
||||||
mypy gen
|
mypy gen
|
||||||
|
|
||||||
- name: mypy theater
|
|
||||||
run: |
|
|
||||||
./venv/scripts/activate
|
|
||||||
mypy theater
|
|
||||||
|
|
||||||
- name: update build number
|
- name: update build number
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@ -43,11 +43,6 @@ jobs:
|
|||||||
./venv/scripts/activate
|
./venv/scripts/activate
|
||||||
mypy gen
|
mypy gen
|
||||||
|
|
||||||
- name: mypy theater
|
|
||||||
run: |
|
|
||||||
./venv/scripts/activate
|
|
||||||
mypy theater
|
|
||||||
|
|
||||||
- name: Build binaries
|
- name: Build binaries
|
||||||
run: |
|
run: |
|
||||||
./venv/scripts/activate
|
./venv/scripts/activate
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import math
|
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
@ -7,8 +6,7 @@ from typing import Dict, List
|
|||||||
|
|
||||||
from dcs.action import Coalition
|
from dcs.action import Coalition
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.task import CAP, CAS, PinpointStrike, Task
|
from dcs.task import CAP, CAS, PinpointStrike
|
||||||
from dcs.unittype import UnitType
|
|
||||||
from dcs.vehicles import AirDefence
|
from dcs.vehicles import AirDefence
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
@ -21,8 +19,6 @@ from gen.conflictgen import Conflict
|
|||||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||||
from theater import ConflictTheater, ControlPoint
|
|
||||||
from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
|
||||||
from . import persistency
|
from . import persistency
|
||||||
from .debriefing import Debriefing
|
from .debriefing import Debriefing
|
||||||
from .event.event import Event, UnitsDeliveryEvent
|
from .event.event import Event, UnitsDeliveryEvent
|
||||||
@ -30,6 +26,7 @@ from .event.frontlineattack import FrontlineAttackEvent
|
|||||||
from .factions.faction import Faction
|
from .factions.faction import Faction
|
||||||
from .infos.information import Information
|
from .infos.information import Information
|
||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
|
from .theater import ConflictTheater, ControlPoint
|
||||||
from .weather import Conditions, TimeOfDay
|
from .weather import Conditions, TimeOfDay
|
||||||
|
|
||||||
COMMISION_UNIT_VARIETY = 4
|
COMMISION_UNIT_VARIETY = 4
|
||||||
|
|||||||
5
game/theater/__init__.py
Normal file
5
game/theater/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .base import *
|
||||||
|
from .conflicttheater import *
|
||||||
|
from .controlpoint import *
|
||||||
|
from .missiontarget import MissionTarget
|
||||||
|
from .theatergroundobject import SamGroundObject
|
||||||
192
game/theater/base.py
Normal file
192
game/theater/base.py
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
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.vehicles import AirDefence, Armor
|
||||||
|
|
||||||
|
from game import db
|
||||||
|
|
||||||
|
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
||||||
|
PLANES_SCRAMBLE_MIN_BASE = 2
|
||||||
|
PLANES_SCRAMBLE_MAX_BASE = 8
|
||||||
|
PLANES_SCRAMBLE_FACTOR = 0.3
|
||||||
|
|
||||||
|
BASE_MAX_STRENGTH = 1
|
||||||
|
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.commision_points: Dict[Type, float] = {}
|
||||||
|
self.strength = 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_planes(self) -> int:
|
||||||
|
return sum(self.aircraft.values())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_armor(self) -> int:
|
||||||
|
return sum(self.armor.values())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_aa(self) -> int:
|
||||||
|
return sum(self.aa.values())
|
||||||
|
|
||||||
|
def total_units(self, task: Task) -> int:
|
||||||
|
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t in db.UNIT_BY_TASK[task]])
|
||||||
|
|
||||||
|
def total_units_of_type(self, unit_type) -> int:
|
||||||
|
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t == unit_type])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_units(self):
|
||||||
|
return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items())
|
||||||
|
|
||||||
|
def _find_best_unit(self, available_units: Dict[UnitType, int],
|
||||||
|
for_type: Task, count: int) -> Dict[UnitType, int]:
|
||||||
|
if count <= 0:
|
||||||
|
logging.warning("{}: no units for {}".format(self, for_type))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
sorted_units = [key for key in available_units if
|
||||||
|
key in db.UNIT_BY_TASK[for_type]]
|
||||||
|
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
|
||||||
|
|
||||||
|
result: Dict[UnitType, int] = {}
|
||||||
|
for unit_type in sorted_units:
|
||||||
|
existing_count = available_units[unit_type] # type: int
|
||||||
|
if not existing_count:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if count <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
result_unit_count = min(count, existing_count)
|
||||||
|
count -= result_unit_count
|
||||||
|
|
||||||
|
assert result_unit_count > 0
|
||||||
|
result[unit_type] = result.get(unit_type, 0) + result_unit_count
|
||||||
|
|
||||||
|
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]:
|
||||||
|
return self._find_best_unit(self.aircraft, for_type, count)
|
||||||
|
|
||||||
|
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
|
||||||
|
return self._find_best_unit(self.armor, for_type, count)
|
||||||
|
|
||||||
|
def append_commision_points(self, for_type, points: float) -> int:
|
||||||
|
self.commision_points[for_type] = self.commision_points.get(for_type, 0) + points
|
||||||
|
points = self.commision_points[for_type]
|
||||||
|
if points >= 1:
|
||||||
|
self.commision_points[for_type] = points - math.floor(points)
|
||||||
|
return int(math.floor(points))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def filter_units(self, applicable_units: typing.Collection):
|
||||||
|
self.aircraft = {k: v for k, v in self.aircraft.items() if k in applicable_units}
|
||||||
|
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
|
||||||
|
|
||||||
|
def commision_units(self, units: typing.Dict[typing.Any, int]):
|
||||||
|
for value in units.values():
|
||||||
|
assert value > 0
|
||||||
|
assert value == math.floor(value)
|
||||||
|
|
||||||
|
for unit_type, unit_count in units.items():
|
||||||
|
for_task = db.unit_task(unit_type)
|
||||||
|
|
||||||
|
target_dict = None
|
||||||
|
if for_task == CAS or for_task == CAP or for_task == Embarking:
|
||||||
|
target_dict = self.aircraft
|
||||||
|
elif for_task == PinpointStrike:
|
||||||
|
target_dict = self.armor
|
||||||
|
elif for_task == AirDefence:
|
||||||
|
target_dict = self.aa
|
||||||
|
|
||||||
|
assert target_dict is not None
|
||||||
|
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||||
|
|
||||||
|
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]):
|
||||||
|
|
||||||
|
for unit_type, count in units_lost.items():
|
||||||
|
|
||||||
|
if unit_type in self.aircraft:
|
||||||
|
target_array = self.aircraft
|
||||||
|
elif unit_type in self.armor:
|
||||||
|
target_array = self.armor
|
||||||
|
else:
|
||||||
|
print("Base didn't find event type {}".format(unit_type))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if unit_type not in target_array:
|
||||||
|
print("Base didn't find event type {}".format(unit_type))
|
||||||
|
continue
|
||||||
|
|
||||||
|
target_array[unit_type] = max(target_array[unit_type] - count, 0)
|
||||||
|
if target_array[unit_type] == 0:
|
||||||
|
del target_array[unit_type]
|
||||||
|
|
||||||
|
def affect_strength(self, amount):
|
||||||
|
self.strength += amount
|
||||||
|
if self.strength > BASE_MAX_STRENGTH:
|
||||||
|
self.strength = BASE_MAX_STRENGTH
|
||||||
|
elif self.strength <= 0:
|
||||||
|
self.strength = BASE_MIN_STRENGTH
|
||||||
|
|
||||||
|
def set_strength_to_minimum(self) -> None:
|
||||||
|
self.strength = BASE_MIN_STRENGTH
|
||||||
|
|
||||||
|
def scramble_count(self, multiplier: float, task: Task = None) -> int:
|
||||||
|
if task:
|
||||||
|
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
|
||||||
|
else:
|
||||||
|
count = self.total_planes
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def assemble_count(self):
|
||||||
|
return int(self.total_armor * 0.5)
|
||||||
|
|
||||||
|
def assemble_aa_count(self) -> int:
|
||||||
|
# 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]:
|
||||||
|
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))
|
||||||
|
|
||||||
|
def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
||||||
|
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
|
||||||
|
|
||||||
|
def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
||||||
|
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
||||||
|
|
||||||
|
def assemble_attack(self) -> typing.Dict[Armor, int]:
|
||||||
|
return self._find_best_armor(PinpointStrike, self.assemble_count())
|
||||||
|
|
||||||
|
def assemble_defense(self) -> typing.Dict[Armor, int]:
|
||||||
|
count = int(self.total_armor * min(self.strength + 0.5, 1))
|
||||||
|
return self._find_best_armor(PinpointStrike, count)
|
||||||
|
|
||||||
|
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
|
||||||
|
return self._find_best_unit(self.aa, AirDefence, count and min(count, self.total_aa) or self.assemble_aa_count())
|
||||||
515
game/theater/conflicttheater.py
Normal file
515
game/theater/conflicttheater.py
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from itertools import tee
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from dcs.mapping import Point
|
||||||
|
from dcs.terrain import (
|
||||||
|
caucasus,
|
||||||
|
nevada,
|
||||||
|
normandy,
|
||||||
|
persiangulf,
|
||||||
|
syria,
|
||||||
|
thechannel,
|
||||||
|
)
|
||||||
|
from dcs.terrain.terrain import Terrain
|
||||||
|
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
from .controlpoint import ControlPoint, MissionTarget
|
||||||
|
from .landmap import Landmap, load_landmap, poly_contains
|
||||||
|
|
||||||
|
Numeric = Union[int, float]
|
||||||
|
|
||||||
|
SIZE_TINY = 150
|
||||||
|
SIZE_SMALL = 600
|
||||||
|
SIZE_REGULAR = 1000
|
||||||
|
SIZE_BIG = 2000
|
||||||
|
SIZE_LARGE = 3000
|
||||||
|
|
||||||
|
IMPORTANCE_LOW = 1
|
||||||
|
IMPORTANCE_MEDIUM = 1.2
|
||||||
|
IMPORTANCE_HIGH = 1.4
|
||||||
|
|
||||||
|
"""
|
||||||
|
ALL_RADIALS = [0, 45, 90, 135, 180, 225, 270, 315, ]
|
||||||
|
COAST_NS_E = [45, 90, 135, ]
|
||||||
|
COAST_EW_N = [315, 0, 45, ]
|
||||||
|
COAST_NSEW_E = [225, 270, 315, ]
|
||||||
|
COAST_NSEW_W = [45, 90, 135, ]
|
||||||
|
|
||||||
|
COAST_NS_W = [225, 270, 315, ]
|
||||||
|
COAST_EW_S = [135, 180, 225, ]
|
||||||
|
"""
|
||||||
|
|
||||||
|
LAND = [0, 45, 90, 135, 180, 225, 270, 315, ]
|
||||||
|
|
||||||
|
COAST_V_E = [0, 45, 90, 135, 180]
|
||||||
|
COAST_V_W = [180, 225, 270, 315, 0]
|
||||||
|
|
||||||
|
COAST_A_W = [315, 0, 45, 135, 180, 225, 270]
|
||||||
|
COAST_A_E = [0, 45, 90, 135, 180, 225, 315]
|
||||||
|
|
||||||
|
COAST_H_N = [270, 315, 0, 45, 90]
|
||||||
|
COAST_H_S = [90, 135, 180, 225, 270]
|
||||||
|
|
||||||
|
COAST_DL_E = [45, 90, 135, 180, 225]
|
||||||
|
COAST_DL_W = [225, 270, 315, 0, 45]
|
||||||
|
COAST_DR_E = [315, 0, 45, 90, 135]
|
||||||
|
COAST_DR_W = [135, 180, 225, 315]
|
||||||
|
|
||||||
|
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||||
|
|
||||||
|
def pairwise(iterable):
|
||||||
|
"""
|
||||||
|
itertools recipe
|
||||||
|
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
||||||
|
"""
|
||||||
|
a, b = tee(iterable)
|
||||||
|
next(b, None)
|
||||||
|
return zip(a, b)
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictTheater:
|
||||||
|
terrain: Terrain
|
||||||
|
|
||||||
|
reference_points: Dict[Tuple[float, float], Tuple[float, float]]
|
||||||
|
overview_image: str
|
||||||
|
landmap: Optional[Landmap]
|
||||||
|
"""
|
||||||
|
land_poly = None # type: Polygon
|
||||||
|
"""
|
||||||
|
daytime_map: Dict[str, Tuple[int, int]]
|
||||||
|
frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.controlpoints: List[ControlPoint] = []
|
||||||
|
self.frontline_data = FrontLine.load_json_frontlines(self)
|
||||||
|
"""
|
||||||
|
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))
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_controlpoint(self, point: ControlPoint,
|
||||||
|
connected_to: Optional[List[ControlPoint]] = None):
|
||||||
|
if connected_to is None:
|
||||||
|
connected_to = []
|
||||||
|
for connected_point in connected_to:
|
||||||
|
point.connect(to=connected_point)
|
||||||
|
|
||||||
|
self.controlpoints.append(point)
|
||||||
|
|
||||||
|
def find_ground_objects_by_obj_name(self, obj_name):
|
||||||
|
found = []
|
||||||
|
for cp in self.controlpoints:
|
||||||
|
for g in cp.ground_objects:
|
||||||
|
if g.obj_name == obj_name:
|
||||||
|
found.append(g)
|
||||||
|
return found
|
||||||
|
|
||||||
|
def is_in_sea(self, point: Point) -> bool:
|
||||||
|
if not self.landmap:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.is_on_land(point):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for sea in self.landmap[2]:
|
||||||
|
if poly_contains(point.x, point.y, sea):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_on_land(self, point: Point) -> bool:
|
||||||
|
if not self.landmap:
|
||||||
|
return True
|
||||||
|
|
||||||
|
is_point_included = False
|
||||||
|
for inclusion_zone in self.landmap[0]:
|
||||||
|
if poly_contains(point.x, point.y, inclusion_zone):
|
||||||
|
is_point_included = True
|
||||||
|
|
||||||
|
if not is_point_included:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for exclusion_zone in self.landmap[1]:
|
||||||
|
if poly_contains(point.x, point.y, exclusion_zone):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def player_points(self) -> List[ControlPoint]:
|
||||||
|
return [point for point in self.controlpoints if point.captured]
|
||||||
|
|
||||||
|
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
|
||||||
|
for cp in [x for x in self.controlpoints if x.captured == from_player]:
|
||||||
|
for connected_point in [x for x in cp.connected_points if x.captured != from_player]:
|
||||||
|
yield FrontLine(cp, connected_point, self)
|
||||||
|
|
||||||
|
def enemy_points(self) -> List[ControlPoint]:
|
||||||
|
return [point for point in self.controlpoints if not point.captured]
|
||||||
|
|
||||||
|
def add_json_cp(self, theater, p: dict) -> ControlPoint:
|
||||||
|
|
||||||
|
if p["type"] == "airbase":
|
||||||
|
|
||||||
|
airbase = theater.terrain.airports[p["id"]].__class__
|
||||||
|
|
||||||
|
if "radials" in p.keys():
|
||||||
|
radials = p["radials"]
|
||||||
|
else:
|
||||||
|
radials = LAND
|
||||||
|
|
||||||
|
if "size" in p.keys():
|
||||||
|
size = p["size"]
|
||||||
|
else:
|
||||||
|
size = SIZE_REGULAR
|
||||||
|
|
||||||
|
if "importance" in p.keys():
|
||||||
|
importance = p["importance"]
|
||||||
|
else:
|
||||||
|
importance = IMPORTANCE_MEDIUM
|
||||||
|
|
||||||
|
cp = ControlPoint.from_airport(airbase, radials, size, importance)
|
||||||
|
elif p["type"] == "carrier":
|
||||||
|
cp = ControlPoint.carrier("carrier", Point(p["x"], p["y"]), p["id"])
|
||||||
|
else:
|
||||||
|
cp = ControlPoint.lha("lha", Point(p["x"], p["y"]), p["id"])
|
||||||
|
|
||||||
|
if "captured_invert" in p.keys():
|
||||||
|
cp.captured_invert = p["captured_invert"]
|
||||||
|
else:
|
||||||
|
cp.captured_invert = False
|
||||||
|
|
||||||
|
return cp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_json(data: Dict[str, Any]) -> ConflictTheater:
|
||||||
|
theaters = {
|
||||||
|
"Caucasus": CaucasusTheater,
|
||||||
|
"Nevada": NevadaTheater,
|
||||||
|
"Persian Gulf": PersianGulfTheater,
|
||||||
|
"Normandy": NormandyTheater,
|
||||||
|
"The Channel": TheChannelTheater,
|
||||||
|
"Syria": SyriaTheater,
|
||||||
|
}
|
||||||
|
theater = theaters[data["theater"]]
|
||||||
|
t = theater()
|
||||||
|
cps = {}
|
||||||
|
for p in data["player_points"]:
|
||||||
|
cp = t.add_json_cp(theater, p)
|
||||||
|
cp.captured = True
|
||||||
|
cps[p["id"]] = cp
|
||||||
|
t.add_controlpoint(cp)
|
||||||
|
|
||||||
|
for p in data["enemy_points"]:
|
||||||
|
cp = t.add_json_cp(theater, p)
|
||||||
|
cps[p["id"]] = cp
|
||||||
|
t.add_controlpoint(cp)
|
||||||
|
|
||||||
|
for l in data["links"]:
|
||||||
|
cps[l[0]].connect(cps[l[1]])
|
||||||
|
cps[l[1]].connect(cps[l[0]])
|
||||||
|
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
class CaucasusTheater(ConflictTheater):
|
||||||
|
terrain = caucasus.Caucasus()
|
||||||
|
overview_image = "caumap.gif"
|
||||||
|
reference_points = {(-317948.32727306, 635639.37385346): (278.5 * 4, 319 * 4),
|
||||||
|
(-355692.3067714, 617269.96285781): (263 * 4, 352 * 4), }
|
||||||
|
|
||||||
|
landmap = load_landmap("resources\\caulandmap.p")
|
||||||
|
daytime_map = {
|
||||||
|
"dawn": (6, 9),
|
||||||
|
"day": (9, 18),
|
||||||
|
"dusk": (18, 20),
|
||||||
|
"night": (0, 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PersianGulfTheater(ConflictTheater):
|
||||||
|
terrain = persiangulf.PersianGulf()
|
||||||
|
overview_image = "persiangulf.gif"
|
||||||
|
reference_points = {
|
||||||
|
(persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): (
|
||||||
|
772, -1970),
|
||||||
|
(persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), }
|
||||||
|
landmap = load_landmap("resources\\gulflandmap.p")
|
||||||
|
daytime_map = {
|
||||||
|
"dawn": (6, 8),
|
||||||
|
"day": (8, 16),
|
||||||
|
"dusk": (16, 18),
|
||||||
|
"night": (0, 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
class NevadaTheater(ConflictTheater):
|
||||||
|
terrain = nevada.Nevada()
|
||||||
|
overview_image = "nevada.gif"
|
||||||
|
reference_points = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45 * 2, -360 * 2),
|
||||||
|
(nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440 * 2, 80 * 2), }
|
||||||
|
landmap = load_landmap("resources\\nevlandmap.p")
|
||||||
|
daytime_map = {
|
||||||
|
"dawn": (4, 6),
|
||||||
|
"day": (6, 17),
|
||||||
|
"dusk": (17, 18),
|
||||||
|
"night": (0, 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
class NormandyTheater(ConflictTheater):
|
||||||
|
terrain = normandy.Normandy()
|
||||||
|
overview_image = "normandy.gif"
|
||||||
|
reference_points = {(normandy.Needs_Oar_Point.position.x, normandy.Needs_Oar_Point.position.y): (-170, -1000),
|
||||||
|
(normandy.Evreux.position.x, normandy.Evreux.position.y): (2020, 500)}
|
||||||
|
landmap = load_landmap("resources\\normandylandmap.p")
|
||||||
|
daytime_map = {
|
||||||
|
"dawn": (6, 8),
|
||||||
|
"day": (10, 17),
|
||||||
|
"dusk": (17, 18),
|
||||||
|
"night": (0, 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
class TheChannelTheater(ConflictTheater):
|
||||||
|
terrain = thechannel.TheChannel()
|
||||||
|
overview_image = "thechannel.gif"
|
||||||
|
reference_points = {(thechannel.Abbeville_Drucat.position.x, thechannel.Abbeville_Drucat.position.y): (2400, 4100),
|
||||||
|
(thechannel.Detling.position.x, thechannel.Detling.position.y): (1100, 2000)}
|
||||||
|
landmap = load_landmap("resources\\channellandmap.p")
|
||||||
|
daytime_map = {
|
||||||
|
"dawn": (6, 8),
|
||||||
|
"day": (10, 17),
|
||||||
|
"dusk": (17, 18),
|
||||||
|
"night": (0, 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyriaTheater(ConflictTheater):
|
||||||
|
terrain = syria.Syria()
|
||||||
|
overview_image = "syria.gif"
|
||||||
|
reference_points = {(syria.Eyn_Shemer.position.x, syria.Eyn_Shemer.position.y): (1300, 1380),
|
||||||
|
(syria.Tabqa.position.x, syria.Tabqa.position.y): (2060, 570)}
|
||||||
|
landmap = load_landmap("resources\\syrialandmap.p")
|
||||||
|
daytime_map = {
|
||||||
|
"dawn": (6, 8),
|
||||||
|
"day": (8, 16),
|
||||||
|
"dusk": (16, 18),
|
||||||
|
"night": (0, 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ComplexFrontLine:
|
||||||
|
"""
|
||||||
|
Stores data necessary for building a multi-segment frontline.
|
||||||
|
"points" should be ordered from closest to farthest distance originating from start_cp.position
|
||||||
|
"""
|
||||||
|
|
||||||
|
start_cp: ControlPoint
|
||||||
|
points: List[Point]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FrontLineSegment:
|
||||||
|
"""
|
||||||
|
Describes a line segment of a FrontLine
|
||||||
|
"""
|
||||||
|
|
||||||
|
point_a: Point
|
||||||
|
point_b: Point
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attack_heading(self) -> Numeric:
|
||||||
|
"""The heading of the frontline segment from player to enemy control point"""
|
||||||
|
return self.point_a.heading_between_point(self.point_b)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attack_distance(self) -> Numeric:
|
||||||
|
"""Length of the segment"""
|
||||||
|
return self.point_a.distance_to_point(self.point_b)
|
||||||
|
|
||||||
|
|
||||||
|
class FrontLine(MissionTarget):
|
||||||
|
"""Defines a front line location between two control points.
|
||||||
|
Front lines are the area where ground combat happens.
|
||||||
|
Overwrites the entirety of MissionTarget __init__ method to allow for
|
||||||
|
dynamic position calculation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
control_point_a: ControlPoint,
|
||||||
|
control_point_b: ControlPoint,
|
||||||
|
theater: ConflictTheater
|
||||||
|
) -> None:
|
||||||
|
self.control_point_a = control_point_a
|
||||||
|
self.control_point_b = control_point_b
|
||||||
|
self.segments: List[FrontLineSegment] = []
|
||||||
|
self.theater = theater
|
||||||
|
self._build_segments()
|
||||||
|
self.name = f"Front line {control_point_a}/{control_point_b}"
|
||||||
|
|
||||||
|
def is_friendly(self, to_player: bool) -> bool:
|
||||||
|
"""Returns True if the objective is in friendly territory."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
|
yield from [
|
||||||
|
FlightType.CAS,
|
||||||
|
# TODO: FlightType.TROOP_TRANSPORT
|
||||||
|
# TODO: FlightType.EVAC
|
||||||
|
]
|
||||||
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position(self):
|
||||||
|
"""
|
||||||
|
The position where the conflict should occur
|
||||||
|
according to the current strength of each control point.
|
||||||
|
"""
|
||||||
|
return self.point_from_a(self._position_distance)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||||
|
"""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"""
|
||||||
|
return sum(i.attack_distance for i in self.segments)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attack_heading(self):
|
||||||
|
"""The heading of the active attack segment from player to enemy control point"""
|
||||||
|
return self.active_segment.attack_heading
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_segment(self) -> FrontLineSegment:
|
||||||
|
"""The FrontLine segment where there can be an active conflict"""
|
||||||
|
if self._position_distance <= self.segments[0].attack_distance:
|
||||||
|
return self.segments[0]
|
||||||
|
|
||||||
|
remaining_dist = self._position_distance
|
||||||
|
for segment in self.segments:
|
||||||
|
if remaining_dist <= segment.attack_distance:
|
||||||
|
return segment
|
||||||
|
else:
|
||||||
|
remaining_dist -= segment.attack_distance
|
||||||
|
logging.error(
|
||||||
|
"Frontline attack distance is greater than the sum of its segments"
|
||||||
|
)
|
||||||
|
return self.segments[0]
|
||||||
|
|
||||||
|
def point_from_a(self, distance: Numeric) -> Point:
|
||||||
|
"""
|
||||||
|
Returns a point {distance} away from control_point_a along the frontline segments.
|
||||||
|
"""
|
||||||
|
if distance < self.segments[0].attack_distance:
|
||||||
|
return self.control_point_a.position.point_from_heading(
|
||||||
|
self.segments[0].attack_heading, distance
|
||||||
|
)
|
||||||
|
remaining_dist = distance
|
||||||
|
for segment in self.segments:
|
||||||
|
if remaining_dist < segment.attack_distance:
|
||||||
|
return segment.point_a.point_from_heading(
|
||||||
|
segment.attack_heading, remaining_dist
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
remaining_dist -= segment.attack_distance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _position_distance(self) -> float:
|
||||||
|
"""
|
||||||
|
The distance from point "a" where the conflict should occur
|
||||||
|
according to the current strength of each control point
|
||||||
|
"""
|
||||||
|
total_strength = (
|
||||||
|
self.control_point_a.base.strength + self.control_point_b.base.strength
|
||||||
|
)
|
||||||
|
if self.control_point_a.base.strength == 0:
|
||||||
|
return self._adjust_for_min_dist(0)
|
||||||
|
if self.control_point_b.base.strength == 0:
|
||||||
|
return self._adjust_for_min_dist(self.attack_distance)
|
||||||
|
strength_pct = self.control_point_a.base.strength / total_strength
|
||||||
|
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
|
||||||
|
|
||||||
|
def _adjust_for_min_dist(self, distance: Numeric) -> Numeric:
|
||||||
|
"""
|
||||||
|
Ensures the frontline conflict is never located within the minimum distance
|
||||||
|
constant of either end control point.
|
||||||
|
"""
|
||||||
|
if (distance > self.attack_distance / 2) and (
|
||||||
|
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
|
||||||
|
):
|
||||||
|
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
|
||||||
|
elif (distance < self.attack_distance / 2) and (
|
||||||
|
distance < FRONTLINE_MIN_CP_DISTANCE
|
||||||
|
):
|
||||||
|
distance = FRONTLINE_MIN_CP_DISTANCE
|
||||||
|
return distance
|
||||||
|
|
||||||
|
def _build_segments(self) -> None:
|
||||||
|
"""Create line segments for the frontline"""
|
||||||
|
control_point_ids = "|".join(
|
||||||
|
[str(self.control_point_a.id), str(self.control_point_b.id)]
|
||||||
|
) # from_cp.id|to_cp.id
|
||||||
|
reversed_cp_ids = "|".join(
|
||||||
|
[str(self.control_point_b.id), str(self.control_point_a.id)]
|
||||||
|
)
|
||||||
|
complex_frontlines = self.theater.frontline_data
|
||||||
|
if (complex_frontlines) and (
|
||||||
|
(control_point_ids in complex_frontlines)
|
||||||
|
or (reversed_cp_ids in complex_frontlines)
|
||||||
|
):
|
||||||
|
# The frontline segments must be stored in the correct order for the distance algorithms to work.
|
||||||
|
# The points in the frontline are ordered from the id before the | to the id after.
|
||||||
|
# First, check if control point id pair matches in order, and create segments if a match is found.
|
||||||
|
if control_point_ids in complex_frontlines:
|
||||||
|
point_pairs = pairwise(complex_frontlines[control_point_ids].points)
|
||||||
|
for i in point_pairs:
|
||||||
|
self.segments.append(FrontLineSegment(i[0], i[1]))
|
||||||
|
# Check the reverse order and build in reverse if found.
|
||||||
|
elif reversed_cp_ids in complex_frontlines:
|
||||||
|
point_pairs = pairwise(
|
||||||
|
reversed(complex_frontlines[reversed_cp_ids].points)
|
||||||
|
)
|
||||||
|
for i in point_pairs:
|
||||||
|
self.segments.append(FrontLineSegment(i[0], i[1]))
|
||||||
|
# If no complex frontline has been configured, fall back to the old straight line method.
|
||||||
|
else:
|
||||||
|
self.segments.append(
|
||||||
|
FrontLineSegment(
|
||||||
|
self.control_point_a.position, self.control_point_b.position
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_json_frontlines(
|
||||||
|
theater: ConflictTheater
|
||||||
|
) -> Optional[Dict[str, ComplexFrontLine]]:
|
||||||
|
"""Load complex frontlines from json"""
|
||||||
|
try:
|
||||||
|
path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json")
|
||||||
|
with open(path, "r") as file:
|
||||||
|
logging.debug(f"Loading frontline from {path}...")
|
||||||
|
data = json.load(file)
|
||||||
|
return {
|
||||||
|
frontline: ComplexFrontLine(
|
||||||
|
data[frontline]["start_cp"],
|
||||||
|
[Point(i[0], i[1]) for i in data[frontline]["points"]],
|
||||||
|
)
|
||||||
|
for frontline in data
|
||||||
|
}
|
||||||
|
except OSError:
|
||||||
|
logging.warning(
|
||||||
|
f"Unable to load preset frontlines for {theater.terrain.name}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
262
game/theater/controlpoint.py
Normal file
262
game/theater/controlpoint.py
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import re
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Dict, Iterator, List, TYPE_CHECKING
|
||||||
|
|
||||||
|
from dcs.mapping import Point
|
||||||
|
from dcs.ships import (
|
||||||
|
CVN_74_John_C__Stennis,
|
||||||
|
CV_1143_5_Admiral_Kuznetsov,
|
||||||
|
LHA_1_Tarawa,
|
||||||
|
Type_071_Amphibious_Transport_Dock,
|
||||||
|
)
|
||||||
|
from dcs.terrain.terrain import Airport
|
||||||
|
|
||||||
|
from game import db
|
||||||
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
|
from .base import Base
|
||||||
|
from .missiontarget import MissionTarget
|
||||||
|
from .theatergroundobject import (
|
||||||
|
BaseDefenseGroundObject,
|
||||||
|
TheaterGroundObject,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
class ControlPointType(Enum):
|
||||||
|
AIRBASE = 0 # An airbase with slots for everything
|
||||||
|
AIRCRAFT_CARRIER_GROUP = 1 # A group with a Stennis type carrier (F/A-18, F-14 compatible)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class ControlPoint(MissionTarget):
|
||||||
|
|
||||||
|
position = None # type: Point
|
||||||
|
name = None # type: str
|
||||||
|
|
||||||
|
captured = False
|
||||||
|
has_frontline = True
|
||||||
|
frontline_offset = 0.0
|
||||||
|
|
||||||
|
alt = 0
|
||||||
|
|
||||||
|
def __init__(self, id: int, name: str, position: Point,
|
||||||
|
at: db.StartingPosition, radials: List[int], size: int,
|
||||||
|
importance: float, has_frontline=True,
|
||||||
|
cptype=ControlPointType.AIRBASE):
|
||||||
|
super().__init__(" ".join(re.split(r" |-", name)[:2]), position)
|
||||||
|
self.id = id
|
||||||
|
self.full_name = name
|
||||||
|
self.at = at
|
||||||
|
self.connected_objectives: List[TheaterGroundObject] = []
|
||||||
|
self.base_defenses: List[BaseDefenseGroundObject] = []
|
||||||
|
|
||||||
|
self.size = size
|
||||||
|
self.importance = importance
|
||||||
|
self.captured = False
|
||||||
|
self.captured_invert = False
|
||||||
|
self.has_frontline = has_frontline
|
||||||
|
self.radials = radials
|
||||||
|
self.connected_points: List[ControlPoint] = []
|
||||||
|
self.base: Base = Base()
|
||||||
|
self.cptype = cptype
|
||||||
|
self.stances: Dict[int, CombatStance] = {}
|
||||||
|
self.airport = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ground_objects(self) -> List[TheaterGroundObject]:
|
||||||
|
return list(
|
||||||
|
itertools.chain(self.connected_objectives, self.base_defenses))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def carrier(cls, name: str, at: Point, id: int):
|
||||||
|
import game.theater.conflicttheater
|
||||||
|
cp = cls(id, name, at, at, game.theater.conflicttheater.LAND, game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||||
|
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
|
||||||
|
return cp
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def lha(cls, name: str, at: Point, id: int):
|
||||||
|
import game.theater.conflicttheater
|
||||||
|
cp = cls(id, name, at, at, game.theater.conflicttheater.LAND, game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||||
|
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
|
||||||
|
return cp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def heading(self):
|
||||||
|
if self.cptype == ControlPointType.AIRBASE:
|
||||||
|
return self.airport.runways[0].heading
|
||||||
|
elif self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
|
||||||
|
return 0 # TODO compute heading
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_global(self):
|
||||||
|
return not self.connected_points
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_carrier(self):
|
||||||
|
"""
|
||||||
|
:return: Whether this control point is an aircraft carrier
|
||||||
|
"""
|
||||||
|
return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_fleet(self):
|
||||||
|
"""
|
||||||
|
:return: Whether this control point is a boat (mobile)
|
||||||
|
"""
|
||||||
|
return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_lha(self):
|
||||||
|
"""
|
||||||
|
:return: Whether this control point is an LHA
|
||||||
|
"""
|
||||||
|
return self.cptype in [ControlPointType.LHA_GROUP]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sea_radials(self) -> List[int]:
|
||||||
|
# TODO: fix imports
|
||||||
|
all_radials = [0, 45, 90, 135, 180, 225, 270, 315, ]
|
||||||
|
result = []
|
||||||
|
for r in all_radials:
|
||||||
|
if r not in self.radials:
|
||||||
|
result.append(r)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_aircraft_slots(self):
|
||||||
|
"""
|
||||||
|
:return: The maximum number of aircraft that can be stored in this control point
|
||||||
|
"""
|
||||||
|
if self.cptype == ControlPointType.AIRBASE:
|
||||||
|
return len(self.airport.parking_slots)
|
||||||
|
elif self.is_lha:
|
||||||
|
return 20
|
||||||
|
elif self.is_carrier:
|
||||||
|
return 90
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def connect(self, to):
|
||||||
|
self.connected_points.append(to)
|
||||||
|
self.stances[to.id] = CombatStance.DEFENSIVE
|
||||||
|
|
||||||
|
def has_runway(self):
|
||||||
|
"""
|
||||||
|
Check whether this control point can have aircraft taking off or landing.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] :
|
||||||
|
for g in self.ground_objects:
|
||||||
|
if g.dcs_identifier in ["CARRIER", "LHA"]:
|
||||||
|
for group in g.groups:
|
||||||
|
for u in group.units:
|
||||||
|
if db.unit_type_from_name(u.type) in [CVN_74_John_C__Stennis, LHA_1_Tarawa, CV_1143_5_Admiral_Kuznetsov, Type_071_Amphibious_Transport_Dock]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
elif self.cptype in [ControlPointType.AIRBASE, ControlPointType.FARP]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_carrier_group_name(self):
|
||||||
|
"""
|
||||||
|
Get the carrier group name if the airbase is a carrier
|
||||||
|
:return: Carrier group name
|
||||||
|
"""
|
||||||
|
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] :
|
||||||
|
for g in self.ground_objects:
|
||||||
|
if g.dcs_identifier == "CARRIER":
|
||||||
|
for group in g.groups:
|
||||||
|
for u in group.units:
|
||||||
|
if db.unit_type_from_name(u.type) in [CVN_74_John_C__Stennis, CV_1143_5_Admiral_Kuznetsov]:
|
||||||
|
return group.name
|
||||||
|
elif g.dcs_identifier == "LHA":
|
||||||
|
for group in g.groups:
|
||||||
|
for u in group.units:
|
||||||
|
if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]:
|
||||||
|
return group.name
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_connected(self, to) -> bool:
|
||||||
|
return to in self.connected_points
|
||||||
|
|
||||||
|
def find_radial(self, heading: int, ignored_radial: int = None) -> int:
|
||||||
|
closest_radial = 0
|
||||||
|
closest_radial_delta = 360
|
||||||
|
for radial in [x for x in self.radials if x != ignored_radial]:
|
||||||
|
delta = abs(radial - heading)
|
||||||
|
if delta < closest_radial_delta:
|
||||||
|
closest_radial = radial
|
||||||
|
closest_radial_delta = delta
|
||||||
|
|
||||||
|
return closest_radial
|
||||||
|
|
||||||
|
def find_ground_objects_by_obj_name(self, obj_name):
|
||||||
|
found = []
|
||||||
|
for g in self.ground_objects:
|
||||||
|
if g.obj_name == obj_name:
|
||||||
|
found.append(g)
|
||||||
|
return found
|
||||||
|
|
||||||
|
def is_friendly(self, to_player: bool) -> bool:
|
||||||
|
return self.captured == to_player
|
||||||
|
|
||||||
|
def capture(self, game: Game, for_player: bool) -> None:
|
||||||
|
if for_player:
|
||||||
|
self.captured = True
|
||||||
|
else:
|
||||||
|
self.captured = False
|
||||||
|
|
||||||
|
self.base.set_strength_to_minimum()
|
||||||
|
|
||||||
|
self.base.aircraft = {}
|
||||||
|
self.base.armor = {}
|
||||||
|
|
||||||
|
# Handle cyclic dependency.
|
||||||
|
from .start_generator import BaseDefenseGenerator
|
||||||
|
self.base_defenses = []
|
||||||
|
BaseDefenseGenerator(game, self).generate()
|
||||||
|
|
||||||
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
|
yield from super().mission_types(for_player)
|
||||||
|
if self.is_friendly(for_player):
|
||||||
|
if self.is_fleet:
|
||||||
|
yield from [
|
||||||
|
# TODO: FlightType.INTERCEPTION
|
||||||
|
# TODO: Buddy tanking for the A-4?
|
||||||
|
# TODO: Rescue chopper?
|
||||||
|
# TODO: Inter-ship logistics?
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
yield from [
|
||||||
|
# TODO: FlightType.INTERCEPTION
|
||||||
|
# TODO: FlightType.LOGISTICS
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
if self.is_fleet:
|
||||||
|
yield FlightType.ANTISHIP
|
||||||
|
else:
|
||||||
|
yield from [
|
||||||
|
# TODO: FlightType.STRIKE
|
||||||
|
]
|
||||||
1
game/theater/frontline.py
Normal file
1
game/theater/frontline.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Only here to keep compatibility for save games generated in version 2.2.0"""
|
||||||
@ -34,8 +34,8 @@ from theater import (
|
|||||||
ControlPointType,
|
ControlPointType,
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
)
|
)
|
||||||
from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||||
from theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
EwrGroundObject,
|
EwrGroundObject,
|
||||||
SamGroundObject,
|
SamGroundObject,
|
||||||
BuildingGroundObject,
|
BuildingGroundObject,
|
||||||
336
game/theater/theatergroundobject.py
Normal file
336
game/theater/theatergroundobject.py
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
from typing import Iterator, List, TYPE_CHECKING
|
||||||
|
|
||||||
|
from dcs.mapping import Point
|
||||||
|
from dcs.unit import Unit
|
||||||
|
from dcs.unitgroup import Group
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .controlpoint import ControlPoint
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
from .missiontarget import MissionTarget
|
||||||
|
|
||||||
|
NAME_BY_CATEGORY = {
|
||||||
|
"power": "Power plant",
|
||||||
|
"ammo": "Ammo depot",
|
||||||
|
"fuel": "Fuel depot",
|
||||||
|
"aa": "AA Defense Site",
|
||||||
|
"ware": "Warehouse",
|
||||||
|
"farp": "FARP",
|
||||||
|
"fob": "FOB",
|
||||||
|
"factory": "Factory",
|
||||||
|
"comms": "Comms. tower",
|
||||||
|
"oil": "Oil platform",
|
||||||
|
"derrick": "Derrick",
|
||||||
|
"ww2bunker": "Bunker",
|
||||||
|
"village": "Village",
|
||||||
|
"allycamp": "Camp",
|
||||||
|
"EWR":"EWR",
|
||||||
|
}
|
||||||
|
|
||||||
|
ABBREV_NAME = {
|
||||||
|
"power": "PLANT",
|
||||||
|
"ammo": "AMMO",
|
||||||
|
"fuel": "FUEL",
|
||||||
|
"aa": "AA",
|
||||||
|
"ware": "WARE",
|
||||||
|
"farp": "FARP",
|
||||||
|
"fob": "FOB",
|
||||||
|
"factory": "FACTORY",
|
||||||
|
"comms": "COMMST",
|
||||||
|
"oil": "OILP",
|
||||||
|
"derrick": "DERK",
|
||||||
|
"ww2bunker": "BUNK",
|
||||||
|
"village": "VLG",
|
||||||
|
"allycamp": "CMP",
|
||||||
|
}
|
||||||
|
|
||||||
|
CATEGORY_MAP = {
|
||||||
|
|
||||||
|
# Special cases
|
||||||
|
"CARRIER": ["CARRIER"],
|
||||||
|
"LHA": ["LHA"],
|
||||||
|
"aa": ["AA"],
|
||||||
|
|
||||||
|
# Buildings
|
||||||
|
"power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"],
|
||||||
|
"ware": ["Warehouse", "Hangar A"],
|
||||||
|
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
|
||||||
|
"ammo": [".Ammunition depot", "Hangar B"],
|
||||||
|
"farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"],
|
||||||
|
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
|
||||||
|
"factory": ["Tech combine", "Tech hangar A"],
|
||||||
|
"comms": ["TV tower", "Comms tower M"],
|
||||||
|
"oil": ["Oil platform"],
|
||||||
|
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
|
||||||
|
"ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"],
|
||||||
|
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
|
||||||
|
"allycamp": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TheaterGroundObject(MissionTarget):
|
||||||
|
|
||||||
|
def __init__(self, name: str, category: str, group_id: int, position: Point,
|
||||||
|
heading: int, control_point: ControlPoint, dcs_identifier: str,
|
||||||
|
airbase_group: bool, sea_object: bool) -> None:
|
||||||
|
super().__init__(name, position)
|
||||||
|
self.category = category
|
||||||
|
self.group_id = group_id
|
||||||
|
self.heading = heading
|
||||||
|
self.control_point = control_point
|
||||||
|
self.dcs_identifier = dcs_identifier
|
||||||
|
self.airbase_group = airbase_group
|
||||||
|
self.sea_object = sea_object
|
||||||
|
self.is_dead = False
|
||||||
|
# TODO: There is never more than one group.
|
||||||
|
self.groups: List[Group] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def units(self) -> List[Unit]:
|
||||||
|
"""
|
||||||
|
:return: all the units at this location
|
||||||
|
"""
|
||||||
|
return list(itertools.chain.from_iterable([g.units for g in self.groups]))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group_name(self) -> str:
|
||||||
|
"""The name of the unit group."""
|
||||||
|
return f"{self.category}|{self.group_id}"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return NAME_BY_CATEGORY[self.category]
|
||||||
|
|
||||||
|
def is_same_group(self, identifier: str) -> bool:
|
||||||
|
return self.group_id == identifier
|
||||||
|
|
||||||
|
@property
|
||||||
|
def obj_name(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def faction_color(self) -> str:
|
||||||
|
return "BLUE" if self.control_point.captured else "RED"
|
||||||
|
|
||||||
|
def is_friendly(self, to_player: bool) -> bool:
|
||||||
|
return self.control_point.is_friendly(to_player)
|
||||||
|
|
||||||
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
if self.is_friendly(for_player):
|
||||||
|
yield from [
|
||||||
|
# TODO: FlightType.LOGISTICS
|
||||||
|
# TODO: FlightType.TROOP_TRANSPORT
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
yield from [
|
||||||
|
FlightType.STRIKE,
|
||||||
|
FlightType.BAI,
|
||||||
|
]
|
||||||
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildingGroundObject(TheaterGroundObject):
|
||||||
|
def __init__(self, name: str, category: str, group_id: int, object_id: int,
|
||||||
|
position: Point, heading: int, control_point: ControlPoint,
|
||||||
|
dcs_identifier: str) -> None:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
category=category,
|
||||||
|
group_id=group_id,
|
||||||
|
position=position,
|
||||||
|
heading=heading,
|
||||||
|
control_point=control_point,
|
||||||
|
dcs_identifier=dcs_identifier,
|
||||||
|
airbase_group=False,
|
||||||
|
sea_object=False
|
||||||
|
)
|
||||||
|
self.object_id = object_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group_name(self) -> str:
|
||||||
|
"""The name of the unit group."""
|
||||||
|
return f"{self.category}|{self.group_id}|{self.object_id}"
|
||||||
|
|
||||||
|
|
||||||
|
class NavalGroundObject(TheaterGroundObject):
|
||||||
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
if not self.is_friendly(for_player):
|
||||||
|
yield FlightType.ANTISHIP
|
||||||
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
|
||||||
|
class GenericCarrierGroundObject(NavalGroundObject):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Why is this both a CP and a TGO?
|
||||||
|
class CarrierGroundObject(GenericCarrierGroundObject):
|
||||||
|
def __init__(self, name: str, group_id: int,
|
||||||
|
control_point: ControlPoint) -> None:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
category="CARRIER",
|
||||||
|
group_id=group_id,
|
||||||
|
position=control_point.position,
|
||||||
|
heading=0,
|
||||||
|
control_point=control_point,
|
||||||
|
dcs_identifier="CARRIER",
|
||||||
|
airbase_group=True,
|
||||||
|
sea_object=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group_name(self) -> str:
|
||||||
|
# Prefix the group names with the side color so Skynet can find them,
|
||||||
|
# add to EWR.
|
||||||
|
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Why is this both a CP and a TGO?
|
||||||
|
class LhaGroundObject(GenericCarrierGroundObject):
|
||||||
|
def __init__(self, name: str, group_id: int,
|
||||||
|
control_point: ControlPoint) -> None:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
category="LHA",
|
||||||
|
group_id=group_id,
|
||||||
|
position=control_point.position,
|
||||||
|
heading=0,
|
||||||
|
control_point=control_point,
|
||||||
|
dcs_identifier="LHA",
|
||||||
|
airbase_group=True,
|
||||||
|
sea_object=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group_name(self) -> str:
|
||||||
|
# Prefix the group names with the side color so Skynet can find them,
|
||||||
|
# add to EWR.
|
||||||
|
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||||
|
|
||||||
|
|
||||||
|
class MissileSiteGroundObject(TheaterGroundObject):
|
||||||
|
def __init__(self, name: str, group_id: int, position: Point,
|
||||||
|
control_point: ControlPoint) -> None:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
category="aa",
|
||||||
|
group_id=group_id,
|
||||||
|
position=position,
|
||||||
|
heading=0,
|
||||||
|
control_point=control_point,
|
||||||
|
dcs_identifier="AA",
|
||||||
|
airbase_group=False,
|
||||||
|
sea_object=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDefenseGroundObject(TheaterGroundObject):
|
||||||
|
"""Base type for all base defenses."""
|
||||||
|
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
class SamGroundObject(BaseDefenseGroundObject):
|
||||||
|
def __init__(self, name: str, group_id: int, position: Point,
|
||||||
|
control_point: ControlPoint, for_airbase: bool) -> None:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
category="aa",
|
||||||
|
group_id=group_id,
|
||||||
|
position=position,
|
||||||
|
heading=0,
|
||||||
|
control_point=control_point,
|
||||||
|
dcs_identifier="AA",
|
||||||
|
airbase_group=for_airbase,
|
||||||
|
sea_object=False
|
||||||
|
)
|
||||||
|
# Set by the SAM unit generator if the generated group is compatible
|
||||||
|
# with Skynet.
|
||||||
|
self.skynet_capable = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group_name(self) -> str:
|
||||||
|
if self.skynet_capable:
|
||||||
|
# Prefix the group names of SAM sites with the side color so Skynet
|
||||||
|
# can find them.
|
||||||
|
return f"{self.faction_color}|SAM|{self.group_id}"
|
||||||
|
else:
|
||||||
|
return super().group_name
|
||||||
|
|
||||||
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
if not self.is_friendly(for_player):
|
||||||
|
yield FlightType.DEAD
|
||||||
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
|
||||||
|
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||||
|
def __init__(self, name: str, group_id: int, position: Point,
|
||||||
|
control_point: ControlPoint, for_airbase: bool) -> None:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
category="aa",
|
||||||
|
group_id=group_id,
|
||||||
|
position=position,
|
||||||
|
heading=0,
|
||||||
|
control_point=control_point,
|
||||||
|
dcs_identifier="AA",
|
||||||
|
airbase_group=for_airbase,
|
||||||
|
sea_object=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EwrGroundObject(BaseDefenseGroundObject):
|
||||||
|
def __init__(self, name: str, group_id: int, position: Point,
|
||||||
|
control_point: ControlPoint) -> None:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
category="EWR",
|
||||||
|
group_id=group_id,
|
||||||
|
position=position,
|
||||||
|
heading=0,
|
||||||
|
control_point=control_point,
|
||||||
|
dcs_identifier="EWR",
|
||||||
|
airbase_group=True,
|
||||||
|
sea_object=False
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group_name(self) -> str:
|
||||||
|
# Prefix the group names with the side color so Skynet can find them.
|
||||||
|
return f"{self.faction_color}|{super().group_name}"
|
||||||
|
|
||||||
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
if not self.is_friendly(for_player):
|
||||||
|
yield FlightType.DEAD
|
||||||
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
|
||||||
|
class ShipGroundObject(NavalGroundObject):
|
||||||
|
def __init__(self, name: str, group_id: int, position: Point,
|
||||||
|
control_point: ControlPoint) -> None:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
category="aa",
|
||||||
|
group_id=group_id,
|
||||||
|
position=position,
|
||||||
|
heading=0,
|
||||||
|
control_point=control_point,
|
||||||
|
dcs_identifier="AA",
|
||||||
|
airbase_group=False,
|
||||||
|
sea_object=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group_name(self) -> str:
|
||||||
|
# Prefix the group names with the side color so Skynet can find them,
|
||||||
|
# add to EWR.
|
||||||
|
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||||
@ -84,7 +84,7 @@ from gen.flights.flight import (
|
|||||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||||
from gen.runways import RunwayData
|
from gen.runways import RunwayData
|
||||||
from theater import TheaterGroundObject
|
from theater import TheaterGroundObject
|
||||||
from theater.controlpoint import ControlPoint, ControlPointType
|
from game.theater.controlpoint import ControlPoint, ControlPointType
|
||||||
from .conflictgen import Conflict
|
from .conflictgen import Conflict
|
||||||
from .flights.flightplan import (
|
from .flights.flightplan import (
|
||||||
CasFlightPlan,
|
CasFlightPlan,
|
||||||
|
|||||||
@ -16,7 +16,7 @@ from typing import Dict, List, Optional
|
|||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
|
|
||||||
from theater.missiontarget import MissionTarget
|
from game.theater.missiontarget import MissionTarget
|
||||||
from .flights.flight import Flight, FlightType
|
from .flights.flight import Flight, FlightType
|
||||||
from .flights.flightplan import FormationFlightPlan
|
from .flights.flightplan import FormationFlightPlan
|
||||||
|
|
||||||
|
|||||||
@ -2,19 +2,18 @@
|
|||||||
Briefing generation logic
|
Briefing generation logic
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import random
|
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from theater import FrontLine
|
from typing import Dict, List, TYPE_CHECKING
|
||||||
from typing import List, Dict, TYPE_CHECKING
|
|
||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
||||||
|
|
||||||
from dcs.mission import Mission
|
from dcs.mission import Mission
|
||||||
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
|
||||||
|
from game.theater import ControlPoint, FrontLine
|
||||||
from .aircraft import FlightData
|
from .aircraft import FlightData
|
||||||
from .airsupportgen import AwacsInfo, TankerInfo
|
from .airsupportgen import AwacsInfo, TankerInfo
|
||||||
from .armor import JtacInfo
|
from .armor import JtacInfo
|
||||||
from theater import ControlPoint
|
|
||||||
from .ground_forces.combat_stance import CombatStance
|
from .ground_forces.combat_stance import CombatStance
|
||||||
from .radios import RadioFrequency
|
from .radios import RadioFrequency
|
||||||
from .runways import RunwayData
|
from .runways import RunwayData
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from dcs.ships import (
|
|||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from gen.fleet.dd_group import DDGroupGenerator
|
from gen.fleet.dd_group import DDGroupGenerator
|
||||||
from gen.sam.group_generator import ShipGroupGenerator
|
from gen.sam.group_generator import ShipGroupGenerator
|
||||||
from theater.theatergroundobject import TheaterGroundObject
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.game import Game
|
from game.game import Game
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from theater.theatergroundobject import TheaterGroundObject
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
|
|
||||||
from gen.sam.group_generator import ShipGroupGenerator
|
from gen.sam.group_generator import ShipGroupGenerator
|
||||||
from dcs.unittype import ShipType
|
from dcs.unittype import ShipType
|
||||||
|
|||||||
@ -16,7 +16,7 @@ from dcs.ships import (
|
|||||||
from gen.fleet.dd_group import DDGroupGenerator
|
from gen.fleet.dd_group import DDGroupGenerator
|
||||||
from gen.sam.group_generator import ShipGroupGenerator
|
from gen.sam.group_generator import ShipGroupGenerator
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from theater.theatergroundobject import TheaterGroundObject
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@ -55,7 +55,7 @@ from theater import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Avoid importing some types that cause circular imports unless type checking.
|
# Avoid importing some types that cause circular imports unless type checking.
|
||||||
from theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
EwrGroundObject,
|
EwrGroundObject,
|
||||||
NavalGroundObject, VehicleGroupGroundObject,
|
NavalGroundObject, VehicleGroupGroundObject,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from dcs.point import MovingPoint, PointAction
|
|||||||
from dcs.unittype import FlyingType
|
from dcs.unittype import FlyingType
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
from theater.controlpoint import ControlPoint, MissionTarget
|
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
|
|||||||
@ -27,7 +27,7 @@ from theater import (
|
|||||||
SamGroundObject,
|
SamGroundObject,
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
)
|
)
|
||||||
from theater.theatergroundobject import EwrGroundObject
|
from game.theater.theatergroundobject import EwrGroundObject
|
||||||
from .closestairfields import ObjectiveDistanceCache
|
from .closestairfields import ObjectiveDistanceCache
|
||||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||||
from .traveltime import GroundSpeed, TravelTime
|
from .traveltime import GroundSpeed, TravelTime
|
||||||
|
|||||||
@ -28,7 +28,7 @@ from game import db
|
|||||||
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
||||||
from game.db import unit_type_from_name
|
from game.db import unit_type_from_name
|
||||||
from theater import ControlPoint, TheaterGroundObject
|
from theater import ControlPoint, TheaterGroundObject
|
||||||
from theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
BuildingGroundObject, CarrierGroundObject,
|
BuildingGroundObject, CarrierGroundObject,
|
||||||
GenericCarrierGroundObject,
|
GenericCarrierGroundObject,
|
||||||
LhaGroundObject, ShipGroundObject,
|
LhaGroundObject, ShipGroundObject,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from abc import ABC
|
|||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from gen.sam.group_generator import GroupGenerator
|
from gen.sam.group_generator import GroupGenerator
|
||||||
from theater.theatergroundobject import SamGroundObject
|
from game.theater.theatergroundobject import SamGroundObject
|
||||||
|
|
||||||
|
|
||||||
class GenericSamGroupGenerator(GroupGenerator, ABC):
|
class GenericSamGroupGenerator(GroupGenerator, ABC):
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dcs import unitgroup
|
from dcs import unitgroup
|
||||||
from dcs.point import PointAction
|
from dcs.point import PointAction
|
||||||
@ -9,7 +9,7 @@ from dcs.unit import Vehicle, Ship
|
|||||||
from dcs.unittype import VehicleType
|
from dcs.unittype import VehicleType
|
||||||
|
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from theater.theatergroundobject import TheaterGroundObject
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.game import Game
|
from game.game import Game
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
import random
|
import random
|
||||||
from typing import List, Optional, Type
|
from typing import List, Optional, Type
|
||||||
|
|
||||||
from dcs.vehicles import AirDefence
|
|
||||||
from dcs.unitgroup import VehicleGroup
|
from dcs.unitgroup import VehicleGroup
|
||||||
|
from dcs.vehicles import AirDefence
|
||||||
|
|
||||||
from game import Game, db
|
from game import Game, db
|
||||||
|
from game.theater import TheaterGroundObject
|
||||||
|
from game.theater.theatergroundobject import SamGroundObject
|
||||||
from gen.sam.aaa_bofors import BoforsGenerator
|
from gen.sam.aaa_bofors import BoforsGenerator
|
||||||
from gen.sam.aaa_flak import FlakGenerator
|
from gen.sam.aaa_flak import FlakGenerator
|
||||||
from gen.sam.aaa_flak18 import Flak18Generator
|
from gen.sam.aaa_flak18 import Flak18Generator
|
||||||
from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator
|
from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator
|
||||||
from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator
|
from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator
|
||||||
from gen.sam.cold_war_flak import EarlyColdWarFlakGenerator, ColdWarFlakGenerator
|
from gen.sam.cold_war_flak import (
|
||||||
|
ColdWarFlakGenerator,
|
||||||
|
EarlyColdWarFlakGenerator,
|
||||||
|
)
|
||||||
from gen.sam.ewrs import (
|
from gen.sam.ewrs import (
|
||||||
BigBirdGenerator,
|
BigBirdGenerator,
|
||||||
BoxSpringGenerator,
|
BoxSpringGenerator,
|
||||||
@ -25,6 +28,7 @@ from gen.sam.ewrs import (
|
|||||||
StraightFlushGenerator,
|
StraightFlushGenerator,
|
||||||
TallRackGenerator,
|
TallRackGenerator,
|
||||||
)
|
)
|
||||||
|
from gen.sam.freya_ewr import FreyaGenerator
|
||||||
from gen.sam.group_generator import GroupGenerator
|
from gen.sam.group_generator import GroupGenerator
|
||||||
from gen.sam.sam_avenger import AvengerGenerator
|
from gen.sam.sam_avenger import AvengerGenerator
|
||||||
from gen.sam.sam_chaparral import ChaparralGenerator
|
from gen.sam.sam_chaparral import ChaparralGenerator
|
||||||
@ -50,9 +54,6 @@ from gen.sam.sam_zsu23 import ZSU23Generator
|
|||||||
from gen.sam.sam_zu23 import ZU23Generator
|
from gen.sam.sam_zu23 import ZU23Generator
|
||||||
from gen.sam.sam_zu23_ural import ZU23UralGenerator
|
from gen.sam.sam_zu23_ural import ZU23UralGenerator
|
||||||
from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator
|
from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator
|
||||||
from gen.sam.freya_ewr import FreyaGenerator
|
|
||||||
from theater import TheaterGroundObject
|
|
||||||
from theater.theatergroundobject import SamGroundObject
|
|
||||||
|
|
||||||
SAM_MAP = {
|
SAM_MAP = {
|
||||||
"HawkGenerator": HawkGenerator,
|
"HawkGenerator": HawkGenerator,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from theater.missiontarget import MissionTarget
|
from game.theater.missiontarget import MissionTarget
|
||||||
from .models import GameModel, PackageModel
|
from .models import GameModel, PackageModel
|
||||||
from .windows.mission.QEditFlightDialog import QEditFlightDialog
|
from .windows.mission.QEditFlightDialog import QEditFlightDialog
|
||||||
from .windows.mission.QPackageDialog import (
|
from .windows.mission.QPackageDialog import (
|
||||||
|
|||||||
@ -16,7 +16,7 @@ from gen.ato import AirTaskingOrder, Package
|
|||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from gen.flights.traveltime import TotEstimator
|
from gen.flights.traveltime import TotEstimator
|
||||||
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
||||||
from theater.missiontarget import MissionTarget
|
from game.theater.missiontarget import MissionTarget
|
||||||
|
|
||||||
|
|
||||||
class DeletableChildModelManager:
|
class DeletableChildModelManager:
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from PySide2.QtGui import QColor, QFont, QPixmap
|
from PySide2.QtGui import QColor, QFont, QPixmap
|
||||||
|
|
||||||
from theater.theatergroundobject import CATEGORY_MAP
|
from game.theater.theatergroundobject import CATEGORY_MAP
|
||||||
from .liberation_theme import get_theme_icons
|
from .liberation_theme import get_theme_icons
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from PySide2.QtWidgets import QComboBox
|
|||||||
|
|
||||||
from dcs.planes import PlaneType
|
from dcs.planes import PlaneType
|
||||||
from game.inventory import GlobalAircraftInventory
|
from game.inventory import GlobalAircraftInventory
|
||||||
from theater.controlpoint import ControlPoint
|
from game.theater.controlpoint import ControlPoint
|
||||||
|
|
||||||
|
|
||||||
class QOriginAirfieldSelector(QComboBox):
|
class QOriginAirfieldSelector(QComboBox):
|
||||||
|
|||||||
@ -40,8 +40,8 @@ from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
|
|||||||
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
|
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from theater import ControlPoint
|
from theater import ControlPoint
|
||||||
from theater.conflicttheater import FrontLine
|
from game.theater.conflicttheater import FrontLine
|
||||||
from theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
EwrGroundObject,
|
EwrGroundObject,
|
||||||
MissileSiteGroundObject,
|
MissileSiteGroundObject,
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from PySide2.QtWidgets import (
|
|||||||
|
|
||||||
from qt_ui.dialogs import Dialog
|
from qt_ui.dialogs import Dialog
|
||||||
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
|
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
|
||||||
from theater.missiontarget import MissionTarget
|
from game.theater.missiontarget import MissionTarget
|
||||||
|
|
||||||
|
|
||||||
class QMapObject(QGraphicsRectItem):
|
class QMapObject(QGraphicsRectItem):
|
||||||
|
|||||||
@ -24,7 +24,7 @@ from qt_ui.uiconstants import EVENT_ICONS
|
|||||||
from qt_ui.widgets.ato import QFlightList
|
from qt_ui.widgets.ato import QFlightList
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
|
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
|
||||||
from theater.missiontarget import MissionTarget
|
from game.theater.missiontarget import MissionTarget
|
||||||
|
|
||||||
|
|
||||||
class QPackageDialog(QDialog):
|
class QPackageDialog(QDialog):
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from PySide2.QtWidgets import QAbstractItemView, QListView
|
|||||||
|
|
||||||
from qt_ui.models import GameModel
|
from qt_ui.models import GameModel
|
||||||
from qt_ui.windows.mission.QFlightItem import QFlightItem
|
from qt_ui.windows.mission.QFlightItem import QFlightItem
|
||||||
from theater.controlpoint import ControlPoint
|
from game.theater.controlpoint import ControlPoint
|
||||||
|
|
||||||
|
|
||||||
class QPlannedFlightsView(QListView):
|
class QPlannedFlightsView(QListView):
|
||||||
|
|||||||
@ -15,7 +15,7 @@ from qt_ui.windows.newgame.QCampaignList import (
|
|||||||
QCampaignList,
|
QCampaignList,
|
||||||
load_campaigns,
|
load_campaigns,
|
||||||
)
|
)
|
||||||
from theater.start_generator import GameGenerator
|
from game.theater.start_generator import GameGenerator
|
||||||
|
|
||||||
jinja_env = Environment(
|
jinja_env = Environment(
|
||||||
loader=FileSystemLoader("resources/ui/templates"),
|
loader=FileSystemLoader("resources/ui/templates"),
|
||||||
|
|||||||
@ -1,5 +1,2 @@
|
|||||||
from .base import *
|
# For save game compatibility. Remove before 2.3.
|
||||||
from .conflicttheater import *
|
from game.theater import *
|
||||||
from .controlpoint import *
|
|
||||||
from .missiontarget import MissionTarget
|
|
||||||
from .theatergroundobject import SamGroundObject
|
|
||||||
|
|||||||
194
theater/base.py
194
theater/base.py
@ -1,192 +1,2 @@
|
|||||||
import itertools
|
# For save compat. Remove in 2.3.
|
||||||
import logging
|
from game.theater.base import *
|
||||||
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.vehicles import AirDefence, Armor
|
|
||||||
|
|
||||||
from game import db
|
|
||||||
|
|
||||||
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
|
||||||
PLANES_SCRAMBLE_MIN_BASE = 2
|
|
||||||
PLANES_SCRAMBLE_MAX_BASE = 8
|
|
||||||
PLANES_SCRAMBLE_FACTOR = 0.3
|
|
||||||
|
|
||||||
BASE_MAX_STRENGTH = 1
|
|
||||||
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.commision_points: Dict[Type, float] = {}
|
|
||||||
self.strength = 1
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_planes(self) -> int:
|
|
||||||
return sum(self.aircraft.values())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_armor(self) -> int:
|
|
||||||
return sum(self.armor.values())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_aa(self) -> int:
|
|
||||||
return sum(self.aa.values())
|
|
||||||
|
|
||||||
def total_units(self, task: Task) -> int:
|
|
||||||
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t in db.UNIT_BY_TASK[task]])
|
|
||||||
|
|
||||||
def total_units_of_type(self, unit_type) -> int:
|
|
||||||
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t == unit_type])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def all_units(self):
|
|
||||||
return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items())
|
|
||||||
|
|
||||||
def _find_best_unit(self, available_units: Dict[UnitType, int],
|
|
||||||
for_type: Task, count: int) -> Dict[UnitType, int]:
|
|
||||||
if count <= 0:
|
|
||||||
logging.warning("{}: no units for {}".format(self, for_type))
|
|
||||||
return {}
|
|
||||||
|
|
||||||
sorted_units = [key for key in available_units if
|
|
||||||
key in db.UNIT_BY_TASK[for_type]]
|
|
||||||
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
|
|
||||||
|
|
||||||
result: Dict[UnitType, int] = {}
|
|
||||||
for unit_type in sorted_units:
|
|
||||||
existing_count = available_units[unit_type] # type: int
|
|
||||||
if not existing_count:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if count <= 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
result_unit_count = min(count, existing_count)
|
|
||||||
count -= result_unit_count
|
|
||||||
|
|
||||||
assert result_unit_count > 0
|
|
||||||
result[unit_type] = result.get(unit_type, 0) + result_unit_count
|
|
||||||
|
|
||||||
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]:
|
|
||||||
return self._find_best_unit(self.aircraft, for_type, count)
|
|
||||||
|
|
||||||
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
|
|
||||||
return self._find_best_unit(self.armor, for_type, count)
|
|
||||||
|
|
||||||
def append_commision_points(self, for_type, points: float) -> int:
|
|
||||||
self.commision_points[for_type] = self.commision_points.get(for_type, 0) + points
|
|
||||||
points = self.commision_points[for_type]
|
|
||||||
if points >= 1:
|
|
||||||
self.commision_points[for_type] = points - math.floor(points)
|
|
||||||
return int(math.floor(points))
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def filter_units(self, applicable_units: typing.Collection):
|
|
||||||
self.aircraft = {k: v for k, v in self.aircraft.items() if k in applicable_units}
|
|
||||||
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
|
|
||||||
|
|
||||||
def commision_units(self, units: typing.Dict[typing.Any, int]):
|
|
||||||
for value in units.values():
|
|
||||||
assert value > 0
|
|
||||||
assert value == math.floor(value)
|
|
||||||
|
|
||||||
for unit_type, unit_count in units.items():
|
|
||||||
for_task = db.unit_task(unit_type)
|
|
||||||
|
|
||||||
target_dict = None
|
|
||||||
if for_task == CAS or for_task == CAP or for_task == Embarking:
|
|
||||||
target_dict = self.aircraft
|
|
||||||
elif for_task == PinpointStrike:
|
|
||||||
target_dict = self.armor
|
|
||||||
elif for_task == AirDefence:
|
|
||||||
target_dict = self.aa
|
|
||||||
|
|
||||||
assert target_dict is not None
|
|
||||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
|
||||||
|
|
||||||
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]):
|
|
||||||
|
|
||||||
for unit_type, count in units_lost.items():
|
|
||||||
|
|
||||||
if unit_type in self.aircraft:
|
|
||||||
target_array = self.aircraft
|
|
||||||
elif unit_type in self.armor:
|
|
||||||
target_array = self.armor
|
|
||||||
else:
|
|
||||||
print("Base didn't find event type {}".format(unit_type))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if unit_type not in target_array:
|
|
||||||
print("Base didn't find event type {}".format(unit_type))
|
|
||||||
continue
|
|
||||||
|
|
||||||
target_array[unit_type] = max(target_array[unit_type] - count, 0)
|
|
||||||
if target_array[unit_type] == 0:
|
|
||||||
del target_array[unit_type]
|
|
||||||
|
|
||||||
def affect_strength(self, amount):
|
|
||||||
self.strength += amount
|
|
||||||
if self.strength > BASE_MAX_STRENGTH:
|
|
||||||
self.strength = BASE_MAX_STRENGTH
|
|
||||||
elif self.strength <= 0:
|
|
||||||
self.strength = BASE_MIN_STRENGTH
|
|
||||||
|
|
||||||
def set_strength_to_minimum(self) -> None:
|
|
||||||
self.strength = BASE_MIN_STRENGTH
|
|
||||||
|
|
||||||
def scramble_count(self, multiplier: float, task: Task = None) -> int:
|
|
||||||
if task:
|
|
||||||
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
|
|
||||||
else:
|
|
||||||
count = self.total_planes
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def assemble_count(self):
|
|
||||||
return int(self.total_armor * 0.5)
|
|
||||||
|
|
||||||
def assemble_aa_count(self) -> int:
|
|
||||||
# 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]:
|
|
||||||
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))
|
|
||||||
|
|
||||||
def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
|
||||||
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
|
|
||||||
|
|
||||||
def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]:
|
|
||||||
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
|
||||||
|
|
||||||
def assemble_attack(self) -> typing.Dict[Armor, int]:
|
|
||||||
return self._find_best_armor(PinpointStrike, self.assemble_count())
|
|
||||||
|
|
||||||
def assemble_defense(self) -> typing.Dict[Armor, int]:
|
|
||||||
count = int(self.total_armor * min(self.strength + 0.5, 1))
|
|
||||||
return self._find_best_armor(PinpointStrike, count)
|
|
||||||
|
|
||||||
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
|
|
||||||
return self._find_best_unit(self.aa, AirDefence, count and min(count, self.total_aa) or self.assemble_aa_count())
|
|
||||||
|
|||||||
@ -1,515 +1,2 @@
|
|||||||
from __future__ import annotations
|
# For save compat. Remove in 2.3.
|
||||||
|
from game.theater.conflicttheater import *
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from itertools import tee
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
|
||||||
|
|
||||||
from dcs.mapping import Point
|
|
||||||
from dcs.terrain import (
|
|
||||||
caucasus,
|
|
||||||
nevada,
|
|
||||||
normandy,
|
|
||||||
persiangulf,
|
|
||||||
syria,
|
|
||||||
thechannel,
|
|
||||||
)
|
|
||||||
from dcs.terrain.terrain import Terrain
|
|
||||||
|
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
from .controlpoint import ControlPoint, MissionTarget
|
|
||||||
from .landmap import Landmap, load_landmap, poly_contains
|
|
||||||
|
|
||||||
Numeric = Union[int, float]
|
|
||||||
|
|
||||||
SIZE_TINY = 150
|
|
||||||
SIZE_SMALL = 600
|
|
||||||
SIZE_REGULAR = 1000
|
|
||||||
SIZE_BIG = 2000
|
|
||||||
SIZE_LARGE = 3000
|
|
||||||
|
|
||||||
IMPORTANCE_LOW = 1
|
|
||||||
IMPORTANCE_MEDIUM = 1.2
|
|
||||||
IMPORTANCE_HIGH = 1.4
|
|
||||||
|
|
||||||
"""
|
|
||||||
ALL_RADIALS = [0, 45, 90, 135, 180, 225, 270, 315, ]
|
|
||||||
COAST_NS_E = [45, 90, 135, ]
|
|
||||||
COAST_EW_N = [315, 0, 45, ]
|
|
||||||
COAST_NSEW_E = [225, 270, 315, ]
|
|
||||||
COAST_NSEW_W = [45, 90, 135, ]
|
|
||||||
|
|
||||||
COAST_NS_W = [225, 270, 315, ]
|
|
||||||
COAST_EW_S = [135, 180, 225, ]
|
|
||||||
"""
|
|
||||||
|
|
||||||
LAND = [0, 45, 90, 135, 180, 225, 270, 315, ]
|
|
||||||
|
|
||||||
COAST_V_E = [0, 45, 90, 135, 180]
|
|
||||||
COAST_V_W = [180, 225, 270, 315, 0]
|
|
||||||
|
|
||||||
COAST_A_W = [315, 0, 45, 135, 180, 225, 270]
|
|
||||||
COAST_A_E = [0, 45, 90, 135, 180, 225, 315]
|
|
||||||
|
|
||||||
COAST_H_N = [270, 315, 0, 45, 90]
|
|
||||||
COAST_H_S = [90, 135, 180, 225, 270]
|
|
||||||
|
|
||||||
COAST_DL_E = [45, 90, 135, 180, 225]
|
|
||||||
COAST_DL_W = [225, 270, 315, 0, 45]
|
|
||||||
COAST_DR_E = [315, 0, 45, 90, 135]
|
|
||||||
COAST_DR_W = [135, 180, 225, 315]
|
|
||||||
|
|
||||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
|
||||||
|
|
||||||
def pairwise(iterable):
|
|
||||||
"""
|
|
||||||
itertools recipe
|
|
||||||
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
|
||||||
"""
|
|
||||||
a, b = tee(iterable)
|
|
||||||
next(b, None)
|
|
||||||
return zip(a, b)
|
|
||||||
|
|
||||||
|
|
||||||
class ConflictTheater:
|
|
||||||
terrain: Terrain
|
|
||||||
|
|
||||||
reference_points: Dict[Tuple[float, float], Tuple[float, float]]
|
|
||||||
overview_image: str
|
|
||||||
landmap: Optional[Landmap]
|
|
||||||
"""
|
|
||||||
land_poly = None # type: Polygon
|
|
||||||
"""
|
|
||||||
daytime_map: Dict[str, Tuple[int, int]]
|
|
||||||
frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.controlpoints: List[ControlPoint] = []
|
|
||||||
self.frontline_data = FrontLine.load_json_frontlines(self)
|
|
||||||
"""
|
|
||||||
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))
|
|
||||||
"""
|
|
||||||
|
|
||||||
def add_controlpoint(self, point: ControlPoint,
|
|
||||||
connected_to: Optional[List[ControlPoint]] = None):
|
|
||||||
if connected_to is None:
|
|
||||||
connected_to = []
|
|
||||||
for connected_point in connected_to:
|
|
||||||
point.connect(to=connected_point)
|
|
||||||
|
|
||||||
self.controlpoints.append(point)
|
|
||||||
|
|
||||||
def find_ground_objects_by_obj_name(self, obj_name):
|
|
||||||
found = []
|
|
||||||
for cp in self.controlpoints:
|
|
||||||
for g in cp.ground_objects:
|
|
||||||
if g.obj_name == obj_name:
|
|
||||||
found.append(g)
|
|
||||||
return found
|
|
||||||
|
|
||||||
def is_in_sea(self, point: Point) -> bool:
|
|
||||||
if not self.landmap:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.is_on_land(point):
|
|
||||||
return False
|
|
||||||
|
|
||||||
for sea in self.landmap[2]:
|
|
||||||
if poly_contains(point.x, point.y, sea):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_on_land(self, point: Point) -> bool:
|
|
||||||
if not self.landmap:
|
|
||||||
return True
|
|
||||||
|
|
||||||
is_point_included = False
|
|
||||||
for inclusion_zone in self.landmap[0]:
|
|
||||||
if poly_contains(point.x, point.y, inclusion_zone):
|
|
||||||
is_point_included = True
|
|
||||||
|
|
||||||
if not is_point_included:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for exclusion_zone in self.landmap[1]:
|
|
||||||
if poly_contains(point.x, point.y, exclusion_zone):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def player_points(self) -> List[ControlPoint]:
|
|
||||||
return [point for point in self.controlpoints if point.captured]
|
|
||||||
|
|
||||||
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
|
|
||||||
for cp in [x for x in self.controlpoints if x.captured == from_player]:
|
|
||||||
for connected_point in [x for x in cp.connected_points if x.captured != from_player]:
|
|
||||||
yield FrontLine(cp, connected_point, self)
|
|
||||||
|
|
||||||
def enemy_points(self) -> List[ControlPoint]:
|
|
||||||
return [point for point in self.controlpoints if not point.captured]
|
|
||||||
|
|
||||||
def add_json_cp(self, theater, p: dict) -> ControlPoint:
|
|
||||||
|
|
||||||
if p["type"] == "airbase":
|
|
||||||
|
|
||||||
airbase = theater.terrain.airports[p["id"]].__class__
|
|
||||||
|
|
||||||
if "radials" in p.keys():
|
|
||||||
radials = p["radials"]
|
|
||||||
else:
|
|
||||||
radials = LAND
|
|
||||||
|
|
||||||
if "size" in p.keys():
|
|
||||||
size = p["size"]
|
|
||||||
else:
|
|
||||||
size = SIZE_REGULAR
|
|
||||||
|
|
||||||
if "importance" in p.keys():
|
|
||||||
importance = p["importance"]
|
|
||||||
else:
|
|
||||||
importance = IMPORTANCE_MEDIUM
|
|
||||||
|
|
||||||
cp = ControlPoint.from_airport(airbase, radials, size, importance)
|
|
||||||
elif p["type"] == "carrier":
|
|
||||||
cp = ControlPoint.carrier("carrier", Point(p["x"], p["y"]), p["id"])
|
|
||||||
else:
|
|
||||||
cp = ControlPoint.lha("lha", Point(p["x"], p["y"]), p["id"])
|
|
||||||
|
|
||||||
if "captured_invert" in p.keys():
|
|
||||||
cp.captured_invert = p["captured_invert"]
|
|
||||||
else:
|
|
||||||
cp.captured_invert = False
|
|
||||||
|
|
||||||
return cp
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_json(data: Dict[str, Any]) -> ConflictTheater:
|
|
||||||
theaters = {
|
|
||||||
"Caucasus": CaucasusTheater,
|
|
||||||
"Nevada": NevadaTheater,
|
|
||||||
"Persian Gulf": PersianGulfTheater,
|
|
||||||
"Normandy": NormandyTheater,
|
|
||||||
"The Channel": TheChannelTheater,
|
|
||||||
"Syria": SyriaTheater,
|
|
||||||
}
|
|
||||||
theater = theaters[data["theater"]]
|
|
||||||
t = theater()
|
|
||||||
cps = {}
|
|
||||||
for p in data["player_points"]:
|
|
||||||
cp = t.add_json_cp(theater, p)
|
|
||||||
cp.captured = True
|
|
||||||
cps[p["id"]] = cp
|
|
||||||
t.add_controlpoint(cp)
|
|
||||||
|
|
||||||
for p in data["enemy_points"]:
|
|
||||||
cp = t.add_json_cp(theater, p)
|
|
||||||
cps[p["id"]] = cp
|
|
||||||
t.add_controlpoint(cp)
|
|
||||||
|
|
||||||
for l in data["links"]:
|
|
||||||
cps[l[0]].connect(cps[l[1]])
|
|
||||||
cps[l[1]].connect(cps[l[0]])
|
|
||||||
|
|
||||||
return t
|
|
||||||
|
|
||||||
|
|
||||||
class CaucasusTheater(ConflictTheater):
|
|
||||||
terrain = caucasus.Caucasus()
|
|
||||||
overview_image = "caumap.gif"
|
|
||||||
reference_points = {(-317948.32727306, 635639.37385346): (278.5 * 4, 319 * 4),
|
|
||||||
(-355692.3067714, 617269.96285781): (263 * 4, 352 * 4), }
|
|
||||||
|
|
||||||
landmap = load_landmap("resources\\caulandmap.p")
|
|
||||||
daytime_map = {
|
|
||||||
"dawn": (6, 9),
|
|
||||||
"day": (9, 18),
|
|
||||||
"dusk": (18, 20),
|
|
||||||
"night": (0, 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PersianGulfTheater(ConflictTheater):
|
|
||||||
terrain = persiangulf.PersianGulf()
|
|
||||||
overview_image = "persiangulf.gif"
|
|
||||||
reference_points = {
|
|
||||||
(persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): (
|
|
||||||
772, -1970),
|
|
||||||
(persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), }
|
|
||||||
landmap = load_landmap("resources\\gulflandmap.p")
|
|
||||||
daytime_map = {
|
|
||||||
"dawn": (6, 8),
|
|
||||||
"day": (8, 16),
|
|
||||||
"dusk": (16, 18),
|
|
||||||
"night": (0, 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
class NevadaTheater(ConflictTheater):
|
|
||||||
terrain = nevada.Nevada()
|
|
||||||
overview_image = "nevada.gif"
|
|
||||||
reference_points = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45 * 2, -360 * 2),
|
|
||||||
(nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440 * 2, 80 * 2), }
|
|
||||||
landmap = load_landmap("resources\\nevlandmap.p")
|
|
||||||
daytime_map = {
|
|
||||||
"dawn": (4, 6),
|
|
||||||
"day": (6, 17),
|
|
||||||
"dusk": (17, 18),
|
|
||||||
"night": (0, 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
class NormandyTheater(ConflictTheater):
|
|
||||||
terrain = normandy.Normandy()
|
|
||||||
overview_image = "normandy.gif"
|
|
||||||
reference_points = {(normandy.Needs_Oar_Point.position.x, normandy.Needs_Oar_Point.position.y): (-170, -1000),
|
|
||||||
(normandy.Evreux.position.x, normandy.Evreux.position.y): (2020, 500)}
|
|
||||||
landmap = load_landmap("resources\\normandylandmap.p")
|
|
||||||
daytime_map = {
|
|
||||||
"dawn": (6, 8),
|
|
||||||
"day": (10, 17),
|
|
||||||
"dusk": (17, 18),
|
|
||||||
"night": (0, 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
class TheChannelTheater(ConflictTheater):
|
|
||||||
terrain = thechannel.TheChannel()
|
|
||||||
overview_image = "thechannel.gif"
|
|
||||||
reference_points = {(thechannel.Abbeville_Drucat.position.x, thechannel.Abbeville_Drucat.position.y): (2400, 4100),
|
|
||||||
(thechannel.Detling.position.x, thechannel.Detling.position.y): (1100, 2000)}
|
|
||||||
landmap = load_landmap("resources\\channellandmap.p")
|
|
||||||
daytime_map = {
|
|
||||||
"dawn": (6, 8),
|
|
||||||
"day": (10, 17),
|
|
||||||
"dusk": (17, 18),
|
|
||||||
"night": (0, 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
class SyriaTheater(ConflictTheater):
|
|
||||||
terrain = syria.Syria()
|
|
||||||
overview_image = "syria.gif"
|
|
||||||
reference_points = {(syria.Eyn_Shemer.position.x, syria.Eyn_Shemer.position.y): (1300, 1380),
|
|
||||||
(syria.Tabqa.position.x, syria.Tabqa.position.y): (2060, 570)}
|
|
||||||
landmap = load_landmap("resources\\syrialandmap.p")
|
|
||||||
daytime_map = {
|
|
||||||
"dawn": (6, 8),
|
|
||||||
"day": (8, 16),
|
|
||||||
"dusk": (16, 18),
|
|
||||||
"night": (0, 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ComplexFrontLine:
|
|
||||||
"""
|
|
||||||
Stores data necessary for building a multi-segment frontline.
|
|
||||||
"points" should be ordered from closest to farthest distance originating from start_cp.position
|
|
||||||
"""
|
|
||||||
|
|
||||||
start_cp: ControlPoint
|
|
||||||
points: List[Point]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FrontLineSegment:
|
|
||||||
"""
|
|
||||||
Describes a line segment of a FrontLine
|
|
||||||
"""
|
|
||||||
|
|
||||||
point_a: Point
|
|
||||||
point_b: Point
|
|
||||||
|
|
||||||
@property
|
|
||||||
def attack_heading(self) -> Numeric:
|
|
||||||
"""The heading of the frontline segment from player to enemy control point"""
|
|
||||||
return self.point_a.heading_between_point(self.point_b)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def attack_distance(self) -> Numeric:
|
|
||||||
"""Length of the segment"""
|
|
||||||
return self.point_a.distance_to_point(self.point_b)
|
|
||||||
|
|
||||||
|
|
||||||
class FrontLine(MissionTarget):
|
|
||||||
"""Defines a front line location between two control points.
|
|
||||||
Front lines are the area where ground combat happens.
|
|
||||||
Overwrites the entirety of MissionTarget __init__ method to allow for
|
|
||||||
dynamic position calculation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
control_point_a: ControlPoint,
|
|
||||||
control_point_b: ControlPoint,
|
|
||||||
theater: ConflictTheater
|
|
||||||
) -> None:
|
|
||||||
self.control_point_a = control_point_a
|
|
||||||
self.control_point_b = control_point_b
|
|
||||||
self.segments: List[FrontLineSegment] = []
|
|
||||||
self.theater = theater
|
|
||||||
self._build_segments()
|
|
||||||
self.name = f"Front line {control_point_a}/{control_point_b}"
|
|
||||||
|
|
||||||
def is_friendly(self, to_player: bool) -> bool:
|
|
||||||
"""Returns True if the objective is in friendly territory."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
||||||
yield from [
|
|
||||||
FlightType.CAS,
|
|
||||||
# TODO: FlightType.TROOP_TRANSPORT
|
|
||||||
# TODO: FlightType.EVAC
|
|
||||||
]
|
|
||||||
yield from super().mission_types(for_player)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def position(self):
|
|
||||||
"""
|
|
||||||
The position where the conflict should occur
|
|
||||||
according to the current strength of each control point.
|
|
||||||
"""
|
|
||||||
return self.point_from_a(self._position_distance)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
|
||||||
"""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"""
|
|
||||||
return sum(i.attack_distance for i in self.segments)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def attack_heading(self):
|
|
||||||
"""The heading of the active attack segment from player to enemy control point"""
|
|
||||||
return self.active_segment.attack_heading
|
|
||||||
|
|
||||||
@property
|
|
||||||
def active_segment(self) -> FrontLineSegment:
|
|
||||||
"""The FrontLine segment where there can be an active conflict"""
|
|
||||||
if self._position_distance <= self.segments[0].attack_distance:
|
|
||||||
return self.segments[0]
|
|
||||||
|
|
||||||
remaining_dist = self._position_distance
|
|
||||||
for segment in self.segments:
|
|
||||||
if remaining_dist <= segment.attack_distance:
|
|
||||||
return segment
|
|
||||||
else:
|
|
||||||
remaining_dist -= segment.attack_distance
|
|
||||||
logging.error(
|
|
||||||
"Frontline attack distance is greater than the sum of its segments"
|
|
||||||
)
|
|
||||||
return self.segments[0]
|
|
||||||
|
|
||||||
def point_from_a(self, distance: Numeric) -> Point:
|
|
||||||
"""
|
|
||||||
Returns a point {distance} away from control_point_a along the frontline segments.
|
|
||||||
"""
|
|
||||||
if distance < self.segments[0].attack_distance:
|
|
||||||
return self.control_point_a.position.point_from_heading(
|
|
||||||
self.segments[0].attack_heading, distance
|
|
||||||
)
|
|
||||||
remaining_dist = distance
|
|
||||||
for segment in self.segments:
|
|
||||||
if remaining_dist < segment.attack_distance:
|
|
||||||
return segment.point_a.point_from_heading(
|
|
||||||
segment.attack_heading, remaining_dist
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
remaining_dist -= segment.attack_distance
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _position_distance(self) -> float:
|
|
||||||
"""
|
|
||||||
The distance from point "a" where the conflict should occur
|
|
||||||
according to the current strength of each control point
|
|
||||||
"""
|
|
||||||
total_strength = (
|
|
||||||
self.control_point_a.base.strength + self.control_point_b.base.strength
|
|
||||||
)
|
|
||||||
if self.control_point_a.base.strength == 0:
|
|
||||||
return self._adjust_for_min_dist(0)
|
|
||||||
if self.control_point_b.base.strength == 0:
|
|
||||||
return self._adjust_for_min_dist(self.attack_distance)
|
|
||||||
strength_pct = self.control_point_a.base.strength / total_strength
|
|
||||||
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
|
|
||||||
|
|
||||||
def _adjust_for_min_dist(self, distance: Numeric) -> Numeric:
|
|
||||||
"""
|
|
||||||
Ensures the frontline conflict is never located within the minimum distance
|
|
||||||
constant of either end control point.
|
|
||||||
"""
|
|
||||||
if (distance > self.attack_distance / 2) and (
|
|
||||||
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
|
|
||||||
):
|
|
||||||
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
|
|
||||||
elif (distance < self.attack_distance / 2) and (
|
|
||||||
distance < FRONTLINE_MIN_CP_DISTANCE
|
|
||||||
):
|
|
||||||
distance = FRONTLINE_MIN_CP_DISTANCE
|
|
||||||
return distance
|
|
||||||
|
|
||||||
def _build_segments(self) -> None:
|
|
||||||
"""Create line segments for the frontline"""
|
|
||||||
control_point_ids = "|".join(
|
|
||||||
[str(self.control_point_a.id), str(self.control_point_b.id)]
|
|
||||||
) # from_cp.id|to_cp.id
|
|
||||||
reversed_cp_ids = "|".join(
|
|
||||||
[str(self.control_point_b.id), str(self.control_point_a.id)]
|
|
||||||
)
|
|
||||||
complex_frontlines = self.theater.frontline_data
|
|
||||||
if (complex_frontlines) and (
|
|
||||||
(control_point_ids in complex_frontlines)
|
|
||||||
or (reversed_cp_ids in complex_frontlines)
|
|
||||||
):
|
|
||||||
# The frontline segments must be stored in the correct order for the distance algorithms to work.
|
|
||||||
# The points in the frontline are ordered from the id before the | to the id after.
|
|
||||||
# First, check if control point id pair matches in order, and create segments if a match is found.
|
|
||||||
if control_point_ids in complex_frontlines:
|
|
||||||
point_pairs = pairwise(complex_frontlines[control_point_ids].points)
|
|
||||||
for i in point_pairs:
|
|
||||||
self.segments.append(FrontLineSegment(i[0], i[1]))
|
|
||||||
# Check the reverse order and build in reverse if found.
|
|
||||||
elif reversed_cp_ids in complex_frontlines:
|
|
||||||
point_pairs = pairwise(
|
|
||||||
reversed(complex_frontlines[reversed_cp_ids].points)
|
|
||||||
)
|
|
||||||
for i in point_pairs:
|
|
||||||
self.segments.append(FrontLineSegment(i[0], i[1]))
|
|
||||||
# If no complex frontline has been configured, fall back to the old straight line method.
|
|
||||||
else:
|
|
||||||
self.segments.append(
|
|
||||||
FrontLineSegment(
|
|
||||||
self.control_point_a.position, self.control_point_b.position
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load_json_frontlines(
|
|
||||||
theater: ConflictTheater
|
|
||||||
) -> Optional[Dict[str, ComplexFrontLine]]:
|
|
||||||
"""Load complex frontlines from json"""
|
|
||||||
try:
|
|
||||||
path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json")
|
|
||||||
with open(path, "r") as file:
|
|
||||||
logging.debug(f"Loading frontline from {path}...")
|
|
||||||
data = json.load(file)
|
|
||||||
return {
|
|
||||||
frontline: ComplexFrontLine(
|
|
||||||
data[frontline]["start_cp"],
|
|
||||||
[Point(i[0], i[1]) for i in data[frontline]["points"]],
|
|
||||||
)
|
|
||||||
for frontline in data
|
|
||||||
}
|
|
||||||
except OSError:
|
|
||||||
logging.warning(
|
|
||||||
f"Unable to load preset frontlines for {theater.terrain.name}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|||||||
@ -1,262 +1,2 @@
|
|||||||
from __future__ import annotations
|
# For save compat. Remove in 2.3.
|
||||||
|
from game.theater.controlpoint import *
|
||||||
import itertools
|
|
||||||
import re
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Dict, Iterator, List, TYPE_CHECKING
|
|
||||||
|
|
||||||
from dcs.mapping import Point
|
|
||||||
from dcs.ships import (
|
|
||||||
CVN_74_John_C__Stennis,
|
|
||||||
CV_1143_5_Admiral_Kuznetsov,
|
|
||||||
LHA_1_Tarawa,
|
|
||||||
Type_071_Amphibious_Transport_Dock,
|
|
||||||
)
|
|
||||||
from dcs.terrain.terrain import Airport
|
|
||||||
|
|
||||||
from game import db
|
|
||||||
from gen.ground_forces.combat_stance import CombatStance
|
|
||||||
from .base import Base
|
|
||||||
from .missiontarget import MissionTarget
|
|
||||||
from .theatergroundobject import (
|
|
||||||
BaseDefenseGroundObject,
|
|
||||||
TheaterGroundObject,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game import Game
|
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
|
|
||||||
|
|
||||||
class ControlPointType(Enum):
|
|
||||||
AIRBASE = 0 # An airbase with slots for everything
|
|
||||||
AIRCRAFT_CARRIER_GROUP = 1 # A group with a Stennis type carrier (F/A-18, F-14 compatible)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class ControlPoint(MissionTarget):
|
|
||||||
|
|
||||||
position = None # type: Point
|
|
||||||
name = None # type: str
|
|
||||||
|
|
||||||
captured = False
|
|
||||||
has_frontline = True
|
|
||||||
frontline_offset = 0.0
|
|
||||||
|
|
||||||
alt = 0
|
|
||||||
|
|
||||||
def __init__(self, id: int, name: str, position: Point,
|
|
||||||
at: db.StartingPosition, radials: List[int], size: int,
|
|
||||||
importance: float, has_frontline=True,
|
|
||||||
cptype=ControlPointType.AIRBASE):
|
|
||||||
super().__init__(" ".join(re.split(r" |-", name)[:2]), position)
|
|
||||||
self.id = id
|
|
||||||
self.full_name = name
|
|
||||||
self.at = at
|
|
||||||
self.connected_objectives: List[TheaterGroundObject] = []
|
|
||||||
self.base_defenses: List[BaseDefenseGroundObject] = []
|
|
||||||
|
|
||||||
self.size = size
|
|
||||||
self.importance = importance
|
|
||||||
self.captured = False
|
|
||||||
self.captured_invert = False
|
|
||||||
self.has_frontline = has_frontline
|
|
||||||
self.radials = radials
|
|
||||||
self.connected_points: List[ControlPoint] = []
|
|
||||||
self.base: Base = Base()
|
|
||||||
self.cptype = cptype
|
|
||||||
self.stances: Dict[int, CombatStance] = {}
|
|
||||||
self.airport = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ground_objects(self) -> List[TheaterGroundObject]:
|
|
||||||
return list(
|
|
||||||
itertools.chain(self.connected_objectives, self.base_defenses))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
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()
|
|
||||||
return obj
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def carrier(cls, name: str, at: Point, id: int):
|
|
||||||
import theater.conflicttheater
|
|
||||||
cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1,
|
|
||||||
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
|
|
||||||
return cp
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def lha(cls, name: str, at: Point, id: int):
|
|
||||||
import theater.conflicttheater
|
|
||||||
cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1,
|
|
||||||
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
|
|
||||||
return cp
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heading(self):
|
|
||||||
if self.cptype == ControlPointType.AIRBASE:
|
|
||||||
return self.airport.runways[0].heading
|
|
||||||
elif self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
|
|
||||||
return 0 # TODO compute heading
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_global(self):
|
|
||||||
return not self.connected_points
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_carrier(self):
|
|
||||||
"""
|
|
||||||
:return: Whether this control point is an aircraft carrier
|
|
||||||
"""
|
|
||||||
return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_fleet(self):
|
|
||||||
"""
|
|
||||||
:return: Whether this control point is a boat (mobile)
|
|
||||||
"""
|
|
||||||
return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_lha(self):
|
|
||||||
"""
|
|
||||||
:return: Whether this control point is an LHA
|
|
||||||
"""
|
|
||||||
return self.cptype in [ControlPointType.LHA_GROUP]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sea_radials(self) -> List[int]:
|
|
||||||
# TODO: fix imports
|
|
||||||
all_radials = [0, 45, 90, 135, 180, 225, 270, 315, ]
|
|
||||||
result = []
|
|
||||||
for r in all_radials:
|
|
||||||
if r not in self.radials:
|
|
||||||
result.append(r)
|
|
||||||
return result
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available_aircraft_slots(self):
|
|
||||||
"""
|
|
||||||
:return: The maximum number of aircraft that can be stored in this control point
|
|
||||||
"""
|
|
||||||
if self.cptype == ControlPointType.AIRBASE:
|
|
||||||
return len(self.airport.parking_slots)
|
|
||||||
elif self.is_lha:
|
|
||||||
return 20
|
|
||||||
elif self.is_carrier:
|
|
||||||
return 90
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def connect(self, to):
|
|
||||||
self.connected_points.append(to)
|
|
||||||
self.stances[to.id] = CombatStance.DEFENSIVE
|
|
||||||
|
|
||||||
def has_runway(self):
|
|
||||||
"""
|
|
||||||
Check whether this control point can have aircraft taking off or landing.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] :
|
|
||||||
for g in self.ground_objects:
|
|
||||||
if g.dcs_identifier in ["CARRIER", "LHA"]:
|
|
||||||
for group in g.groups:
|
|
||||||
for u in group.units:
|
|
||||||
if db.unit_type_from_name(u.type) in [CVN_74_John_C__Stennis, LHA_1_Tarawa, CV_1143_5_Admiral_Kuznetsov, Type_071_Amphibious_Transport_Dock]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
elif self.cptype in [ControlPointType.AIRBASE, ControlPointType.FARP]:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_carrier_group_name(self):
|
|
||||||
"""
|
|
||||||
Get the carrier group name if the airbase is a carrier
|
|
||||||
:return: Carrier group name
|
|
||||||
"""
|
|
||||||
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] :
|
|
||||||
for g in self.ground_objects:
|
|
||||||
if g.dcs_identifier == "CARRIER":
|
|
||||||
for group in g.groups:
|
|
||||||
for u in group.units:
|
|
||||||
if db.unit_type_from_name(u.type) in [CVN_74_John_C__Stennis, CV_1143_5_Admiral_Kuznetsov]:
|
|
||||||
return group.name
|
|
||||||
elif g.dcs_identifier == "LHA":
|
|
||||||
for group in g.groups:
|
|
||||||
for u in group.units:
|
|
||||||
if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]:
|
|
||||||
return group.name
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_connected(self, to) -> bool:
|
|
||||||
return to in self.connected_points
|
|
||||||
|
|
||||||
def find_radial(self, heading: int, ignored_radial: int = None) -> int:
|
|
||||||
closest_radial = 0
|
|
||||||
closest_radial_delta = 360
|
|
||||||
for radial in [x for x in self.radials if x != ignored_radial]:
|
|
||||||
delta = abs(radial - heading)
|
|
||||||
if delta < closest_radial_delta:
|
|
||||||
closest_radial = radial
|
|
||||||
closest_radial_delta = delta
|
|
||||||
|
|
||||||
return closest_radial
|
|
||||||
|
|
||||||
def find_ground_objects_by_obj_name(self, obj_name):
|
|
||||||
found = []
|
|
||||||
for g in self.ground_objects:
|
|
||||||
if g.obj_name == obj_name:
|
|
||||||
found.append(g)
|
|
||||||
return found
|
|
||||||
|
|
||||||
def is_friendly(self, to_player: bool) -> bool:
|
|
||||||
return self.captured == to_player
|
|
||||||
|
|
||||||
def capture(self, game: Game, for_player: bool) -> None:
|
|
||||||
if for_player:
|
|
||||||
self.captured = True
|
|
||||||
else:
|
|
||||||
self.captured = False
|
|
||||||
|
|
||||||
self.base.set_strength_to_minimum()
|
|
||||||
|
|
||||||
self.base.aircraft = {}
|
|
||||||
self.base.armor = {}
|
|
||||||
|
|
||||||
# Handle cyclic dependency.
|
|
||||||
from .start_generator import BaseDefenseGenerator
|
|
||||||
self.base_defenses = []
|
|
||||||
BaseDefenseGenerator(game, self).generate()
|
|
||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
||||||
yield from super().mission_types(for_player)
|
|
||||||
if self.is_friendly(for_player):
|
|
||||||
if self.is_fleet:
|
|
||||||
yield from [
|
|
||||||
# TODO: FlightType.INTERCEPTION
|
|
||||||
# TODO: Buddy tanking for the A-4?
|
|
||||||
# TODO: Rescue chopper?
|
|
||||||
# TODO: Inter-ship logistics?
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
yield from [
|
|
||||||
# TODO: FlightType.INTERCEPTION
|
|
||||||
# TODO: FlightType.LOGISTICS
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
if self.is_fleet:
|
|
||||||
yield FlightType.ANTISHIP
|
|
||||||
else:
|
|
||||||
yield from [
|
|
||||||
# TODO: FlightType.STRIKE
|
|
||||||
]
|
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
"""Only here to keep compatibility for save games generated in version 2.2.0"""
|
# For save compat. Remove in 2.3.
|
||||||
from theater.conflicttheater import *
|
from game.theater.frontline import *
|
||||||
|
from game.theater.conflicttheater import FrontLine
|
||||||
@ -1,336 +1,2 @@
|
|||||||
from __future__ import annotations
|
# For save compat. Remove in 2.3.
|
||||||
|
from game.theater.theatergroundobject import *
|
||||||
import itertools
|
|
||||||
from typing import Iterator, List, TYPE_CHECKING
|
|
||||||
|
|
||||||
from dcs.mapping import Point
|
|
||||||
from dcs.unit import Unit
|
|
||||||
from dcs.unitgroup import Group
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .controlpoint import ControlPoint
|
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
|
|
||||||
from .missiontarget import MissionTarget
|
|
||||||
|
|
||||||
NAME_BY_CATEGORY = {
|
|
||||||
"power": "Power plant",
|
|
||||||
"ammo": "Ammo depot",
|
|
||||||
"fuel": "Fuel depot",
|
|
||||||
"aa": "AA Defense Site",
|
|
||||||
"ware": "Warehouse",
|
|
||||||
"farp": "FARP",
|
|
||||||
"fob": "FOB",
|
|
||||||
"factory": "Factory",
|
|
||||||
"comms": "Comms. tower",
|
|
||||||
"oil": "Oil platform",
|
|
||||||
"derrick": "Derrick",
|
|
||||||
"ww2bunker": "Bunker",
|
|
||||||
"village": "Village",
|
|
||||||
"allycamp": "Camp",
|
|
||||||
"EWR":"EWR",
|
|
||||||
}
|
|
||||||
|
|
||||||
ABBREV_NAME = {
|
|
||||||
"power": "PLANT",
|
|
||||||
"ammo": "AMMO",
|
|
||||||
"fuel": "FUEL",
|
|
||||||
"aa": "AA",
|
|
||||||
"ware": "WARE",
|
|
||||||
"farp": "FARP",
|
|
||||||
"fob": "FOB",
|
|
||||||
"factory": "FACTORY",
|
|
||||||
"comms": "COMMST",
|
|
||||||
"oil": "OILP",
|
|
||||||
"derrick": "DERK",
|
|
||||||
"ww2bunker": "BUNK",
|
|
||||||
"village": "VLG",
|
|
||||||
"allycamp": "CMP",
|
|
||||||
}
|
|
||||||
|
|
||||||
CATEGORY_MAP = {
|
|
||||||
|
|
||||||
# Special cases
|
|
||||||
"CARRIER": ["CARRIER"],
|
|
||||||
"LHA": ["LHA"],
|
|
||||||
"aa": ["AA"],
|
|
||||||
|
|
||||||
# Buildings
|
|
||||||
"power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"],
|
|
||||||
"ware": ["Warehouse", "Hangar A"],
|
|
||||||
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
|
|
||||||
"ammo": [".Ammunition depot", "Hangar B"],
|
|
||||||
"farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"],
|
|
||||||
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
|
|
||||||
"factory": ["Tech combine", "Tech hangar A"],
|
|
||||||
"comms": ["TV tower", "Comms tower M"],
|
|
||||||
"oil": ["Oil platform"],
|
|
||||||
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
|
|
||||||
"ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"],
|
|
||||||
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
|
|
||||||
"allycamp": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TheaterGroundObject(MissionTarget):
|
|
||||||
|
|
||||||
def __init__(self, name: str, category: str, group_id: int, position: Point,
|
|
||||||
heading: int, control_point: ControlPoint, dcs_identifier: str,
|
|
||||||
airbase_group: bool, sea_object: bool) -> None:
|
|
||||||
super().__init__(name, position)
|
|
||||||
self.category = category
|
|
||||||
self.group_id = group_id
|
|
||||||
self.heading = heading
|
|
||||||
self.control_point = control_point
|
|
||||||
self.dcs_identifier = dcs_identifier
|
|
||||||
self.airbase_group = airbase_group
|
|
||||||
self.sea_object = sea_object
|
|
||||||
self.is_dead = False
|
|
||||||
# TODO: There is never more than one group.
|
|
||||||
self.groups: List[Group] = []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def units(self) -> List[Unit]:
|
|
||||||
"""
|
|
||||||
:return: all the units at this location
|
|
||||||
"""
|
|
||||||
return list(itertools.chain.from_iterable([g.units for g in self.groups]))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def group_name(self) -> str:
|
|
||||||
"""The name of the unit group."""
|
|
||||||
return f"{self.category}|{self.group_id}"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return NAME_BY_CATEGORY[self.category]
|
|
||||||
|
|
||||||
def is_same_group(self, identifier: str) -> bool:
|
|
||||||
return self.group_id == identifier
|
|
||||||
|
|
||||||
@property
|
|
||||||
def obj_name(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def faction_color(self) -> str:
|
|
||||||
return "BLUE" if self.control_point.captured else "RED"
|
|
||||||
|
|
||||||
def is_friendly(self, to_player: bool) -> bool:
|
|
||||||
return self.control_point.is_friendly(to_player)
|
|
||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
if self.is_friendly(for_player):
|
|
||||||
yield from [
|
|
||||||
# TODO: FlightType.LOGISTICS
|
|
||||||
# TODO: FlightType.TROOP_TRANSPORT
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
yield from [
|
|
||||||
FlightType.STRIKE,
|
|
||||||
FlightType.BAI,
|
|
||||||
]
|
|
||||||
yield from super().mission_types(for_player)
|
|
||||||
|
|
||||||
|
|
||||||
class BuildingGroundObject(TheaterGroundObject):
|
|
||||||
def __init__(self, name: str, category: str, group_id: int, object_id: int,
|
|
||||||
position: Point, heading: int, control_point: ControlPoint,
|
|
||||||
dcs_identifier: str) -> None:
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
category=category,
|
|
||||||
group_id=group_id,
|
|
||||||
position=position,
|
|
||||||
heading=heading,
|
|
||||||
control_point=control_point,
|
|
||||||
dcs_identifier=dcs_identifier,
|
|
||||||
airbase_group=False,
|
|
||||||
sea_object=False
|
|
||||||
)
|
|
||||||
self.object_id = object_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def group_name(self) -> str:
|
|
||||||
"""The name of the unit group."""
|
|
||||||
return f"{self.category}|{self.group_id}|{self.object_id}"
|
|
||||||
|
|
||||||
|
|
||||||
class NavalGroundObject(TheaterGroundObject):
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
if not self.is_friendly(for_player):
|
|
||||||
yield FlightType.ANTISHIP
|
|
||||||
yield from super().mission_types(for_player)
|
|
||||||
|
|
||||||
|
|
||||||
class GenericCarrierGroundObject(NavalGroundObject):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Why is this both a CP and a TGO?
|
|
||||||
class CarrierGroundObject(GenericCarrierGroundObject):
|
|
||||||
def __init__(self, name: str, group_id: int,
|
|
||||||
control_point: ControlPoint) -> None:
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
category="CARRIER",
|
|
||||||
group_id=group_id,
|
|
||||||
position=control_point.position,
|
|
||||||
heading=0,
|
|
||||||
control_point=control_point,
|
|
||||||
dcs_identifier="CARRIER",
|
|
||||||
airbase_group=True,
|
|
||||||
sea_object=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def group_name(self) -> str:
|
|
||||||
# Prefix the group names with the side color so Skynet can find them,
|
|
||||||
# add to EWR.
|
|
||||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Why is this both a CP and a TGO?
|
|
||||||
class LhaGroundObject(GenericCarrierGroundObject):
|
|
||||||
def __init__(self, name: str, group_id: int,
|
|
||||||
control_point: ControlPoint) -> None:
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
category="LHA",
|
|
||||||
group_id=group_id,
|
|
||||||
position=control_point.position,
|
|
||||||
heading=0,
|
|
||||||
control_point=control_point,
|
|
||||||
dcs_identifier="LHA",
|
|
||||||
airbase_group=True,
|
|
||||||
sea_object=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def group_name(self) -> str:
|
|
||||||
# Prefix the group names with the side color so Skynet can find them,
|
|
||||||
# add to EWR.
|
|
||||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
|
||||||
|
|
||||||
|
|
||||||
class MissileSiteGroundObject(TheaterGroundObject):
|
|
||||||
def __init__(self, name: str, group_id: int, position: Point,
|
|
||||||
control_point: ControlPoint) -> None:
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
category="aa",
|
|
||||||
group_id=group_id,
|
|
||||||
position=position,
|
|
||||||
heading=0,
|
|
||||||
control_point=control_point,
|
|
||||||
dcs_identifier="AA",
|
|
||||||
airbase_group=False,
|
|
||||||
sea_object=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseDefenseGroundObject(TheaterGroundObject):
|
|
||||||
"""Base type for all base defenses."""
|
|
||||||
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
class SamGroundObject(BaseDefenseGroundObject):
|
|
||||||
def __init__(self, name: str, group_id: int, position: Point,
|
|
||||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
category="aa",
|
|
||||||
group_id=group_id,
|
|
||||||
position=position,
|
|
||||||
heading=0,
|
|
||||||
control_point=control_point,
|
|
||||||
dcs_identifier="AA",
|
|
||||||
airbase_group=for_airbase,
|
|
||||||
sea_object=False
|
|
||||||
)
|
|
||||||
# Set by the SAM unit generator if the generated group is compatible
|
|
||||||
# with Skynet.
|
|
||||||
self.skynet_capable = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def group_name(self) -> str:
|
|
||||||
if self.skynet_capable:
|
|
||||||
# Prefix the group names of SAM sites with the side color so Skynet
|
|
||||||
# can find them.
|
|
||||||
return f"{self.faction_color}|SAM|{self.group_id}"
|
|
||||||
else:
|
|
||||||
return super().group_name
|
|
||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
if not self.is_friendly(for_player):
|
|
||||||
yield FlightType.DEAD
|
|
||||||
yield from super().mission_types(for_player)
|
|
||||||
|
|
||||||
|
|
||||||
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
|
||||||
def __init__(self, name: str, group_id: int, position: Point,
|
|
||||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
category="aa",
|
|
||||||
group_id=group_id,
|
|
||||||
position=position,
|
|
||||||
heading=0,
|
|
||||||
control_point=control_point,
|
|
||||||
dcs_identifier="AA",
|
|
||||||
airbase_group=for_airbase,
|
|
||||||
sea_object=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EwrGroundObject(BaseDefenseGroundObject):
|
|
||||||
def __init__(self, name: str, group_id: int, position: Point,
|
|
||||||
control_point: ControlPoint) -> None:
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
category="EWR",
|
|
||||||
group_id=group_id,
|
|
||||||
position=position,
|
|
||||||
heading=0,
|
|
||||||
control_point=control_point,
|
|
||||||
dcs_identifier="EWR",
|
|
||||||
airbase_group=True,
|
|
||||||
sea_object=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def group_name(self) -> str:
|
|
||||||
# Prefix the group names with the side color so Skynet can find them.
|
|
||||||
return f"{self.faction_color}|{super().group_name}"
|
|
||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
if not self.is_friendly(for_player):
|
|
||||||
yield FlightType.DEAD
|
|
||||||
yield from super().mission_types(for_player)
|
|
||||||
|
|
||||||
|
|
||||||
class ShipGroundObject(NavalGroundObject):
|
|
||||||
def __init__(self, name: str, group_id: int, position: Point,
|
|
||||||
control_point: ControlPoint) -> None:
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
category="aa",
|
|
||||||
group_id=group_id,
|
|
||||||
position=position,
|
|
||||||
heading=0,
|
|
||||||
control_point=control_point,
|
|
||||||
dcs_identifier="AA",
|
|
||||||
airbase_group=False,
|
|
||||||
sea_object=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def group_name(self) -> str:
|
|
||||||
# Prefix the group names with the side color so Skynet can find them,
|
|
||||||
# add to EWR.
|
|
||||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
class WeatherForecast:
|
|
||||||
pass
|
|
||||||
Loading…
x
Reference in New Issue
Block a user