dcs_liberation/game/theater/controlpoint.py
Dan Albert df80ec635f Add a new miz file based campaign generator.
Defining a campaign using a miz file instead of as JSON has a number of
advantages:

* Much easier for players to mod their campaigns.
* Easier to see the big picture of how objective locations will be laid
  out, since every control point can be seen at once.
* No need to associate objective locations to control points explicitly;
  the campaign generator can claim objectives for control points based
  on distance.
* Easier to create an IADS that performs well.
* Non-random campaigns are easier to make.

The downside is duplication across campaigns, and a less structured data
format for complex objects. The former is annoying if we have to fix a
bug that appears in a dozen campaigns. It's less an annoyance for
needing to start from scratch since the easiest way to create a campaign
will be to copy the "full" campaign for the given theater and prune it.

So far I've implemented control points, base defenses, and front lines.
Still need to add support for non-base defense TGOs.

This currently doesn't do anything for the `radials` property of the
`ControlPoint` because I'm not sure what those are.
2020-11-19 16:55:21 -08:00

303 lines
10 KiB
Python

from __future__ import annotations
import itertools
import random
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Iterator, List, Optional, 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)
@dataclass
class PresetLocations:
base_garrisons: List[Point] = field(default_factory=list)
base_air_defense: List[Point] = field(default_factory=list)
ewrs: List[Point] = field(default_factory=list)
sams: List[Point] = field(default_factory=list)
coastal_defenses: List[Point] = field(default_factory=list)
strike_locations: List[Point] = field(default_factory=list)
@staticmethod
def _random_from(points: List[Point]) -> Optional[Point]:
if not points:
return None
point = random.choice(points)
points.remove(point)
return point
def random_garrison(self) -> Optional[Point]:
return self._random_from(self.base_garrisons)
def random_base_sam(self) -> Optional[Point]:
return self._random_from(self.base_air_defense)
def random_ewr(self) -> Optional[Point]:
return self._random_from(self.ewrs)
def random_sam(self) -> Optional[Point]:
return self._random_from(self.sams)
def random_coastal_defense(self) -> Optional[Point]:
return self._random_from(self.coastal_defenses)
def random_strike_location(self) -> Optional[Point]:
return self._random_from(self.strike_locations)
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.preset_locations = PresetLocations()
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: ControlPoint) -> None:
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
]