Skip to content

Commit

Permalink
Add campaign support for ferry-only bases.
Browse files Browse the repository at this point in the history
Fixes #3170.
  • Loading branch information
DanAlbert committed Sep 22, 2023
1 parent e43874e commit 2344fc0
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 45 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Saves from 8.x are not compatible with 9.0.0.
## Features/Improvements

* **[Engine]** Support for DCS Open Beta 2.8.8.43489.
* **[Campaign]** Added ferry only control points, which offer campaign designers a way to add squadrons that can be brought in after additional airfields are captured.
* **[Data]** Added support for the ARA Veinticinco de Mayo.
* **[Data]** Changed display name of the AI-only F-15E Strike Eagle for clarity.
* **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone.
Expand Down
11 changes: 10 additions & 1 deletion game/campaignloader/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from game.theater.theaterloader import TheaterLoader
from game.version import CAMPAIGN_FORMAT_VERSION
from .campaignairwingconfig import CampaignAirWingConfig
from .controlpointconfig import ControlPointConfig
from .factionrecommendation import FactionRecommendation
from .mizcampaignloader import MizCampaignLoader

Expand Down Expand Up @@ -123,7 +124,15 @@ def load_theater(self, advanced_iads: bool) -> ConflictTheater:
) from ex

with logged_duration("Importing miz data"):
MizCampaignLoader(self.path.parent / miz, t).populate_theater()
MizCampaignLoader(
self.path.parent / miz,
t,
dict(
ControlPointConfig.iter_from_data(
self.data.get("control_points", {})
)
),
).populate_theater()

# TODO: Move into MizCampaignLoader so this doesn't have unknown initialization
# in ConflictTheater.
Expand Down
90 changes: 90 additions & 0 deletions game/campaignloader/controlpointbuilder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from dcs import Point
from dcs.terrain import Airport

from game.campaignloader.controlpointconfig import ControlPointConfig
from game.theater import (
Airfield,
Carrier,
ConflictTheater,
ControlPoint,
Fob,
Lha,
OffMapSpawn,
)


class ControlPointBuilder:
def __init__(
self, theater: ConflictTheater, configs: dict[str | int, ControlPointConfig]
) -> None:
self.theater = theater
self.config = configs

def create_airfield(self, airport: Airport) -> Airfield:
cp = Airfield(airport, self.theater, starts_blue=airport.is_blue())

# Use the unlimited aircraft option to determine if an airfield should
# be owned by the player when the campaign is "inverted".
cp.captured_invert = airport.unlimited_aircrafts

self._apply_config(airport.id, cp)
return cp

def create_fob(
self,
name: str,
position: Point,
theater: ConflictTheater,
starts_blue: bool,
captured_invert: bool,
) -> Fob:
cp = Fob(name, position, theater, starts_blue)
cp.captured_invert = captured_invert
self._apply_config(name, cp)
return cp

def create_carrier(
self,
name: str,
position: Point,
theater: ConflictTheater,
starts_blue: bool,
captured_invert: bool,
) -> Carrier:
cp = Carrier(name, position, theater, starts_blue)
cp.captured_invert = captured_invert
self._apply_config(name, cp)
return cp

def create_lha(
self,
name: str,
position: Point,
theater: ConflictTheater,
starts_blue: bool,
captured_invert: bool,
) -> Lha:
cp = Lha(name, position, theater, starts_blue)
cp.captured_invert = captured_invert
self._apply_config(name, cp)
return cp

def create_off_map(
self,
name: str,
position: Point,
theater: ConflictTheater,
starts_blue: bool,
captured_invert: bool,
) -> OffMapSpawn:
cp = OffMapSpawn(name, position, theater, starts_blue)
cp.captured_invert = captured_invert
self._apply_config(name, cp)
return cp

def _apply_config(self, cp_id: str | int, control_point: ControlPoint) -> None:
config = self.config.get(cp_id)
if config is None:
return

control_point.ferry_only = config.ferry_only
21 changes: 21 additions & 0 deletions game/campaignloader/controlpointconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from collections.abc import Iterator
from dataclasses import dataclass
from typing import Any


@dataclass(frozen=True)
class ControlPointConfig:
ferry_only: bool

@staticmethod
def from_data(data: dict[str, Any]) -> ControlPointConfig:
return ControlPointConfig(ferry_only=data.get("ferry_only", False))

@staticmethod
def iter_from_data(
data: dict[str | int, Any]
) -> Iterator[tuple[str | int, ControlPointConfig]]:
for name_or_id, cp_data in data.items():
yield name_or_id, ControlPointConfig.from_data(cp_data)
70 changes: 37 additions & 33 deletions game/campaignloader/mizcampaignloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,14 @@
from dcs.planes import F_15C
from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa
from dcs.statics import Fortification, Warehouse
from dcs.terrain import Airport
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed

from game.campaignloader.controlpointbuilder import ControlPointBuilder
from game.campaignloader.controlpointconfig import ControlPointConfig
from game.profiling import logged_duration
from game.scenery_group import SceneryGroup
from game.theater.controlpoint import (
Airfield,
Carrier,
ControlPoint,
Fob,
Lha,
OffMapSpawn,
)
from game.theater.controlpoint import ControlPoint
from game.theater.presetlocation import PresetLocation

if TYPE_CHECKING:
Expand Down Expand Up @@ -92,8 +86,14 @@ class MizCampaignLoader:

STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id

def __init__(self, miz: Path, theater: ConflictTheater) -> None:
def __init__(
self,
miz: Path,
theater: ConflictTheater,
control_point_configs: dict[str | int, ControlPointConfig],
) -> None:
self.theater = theater
self.control_point_builder = ControlPointBuilder(theater, control_point_configs)
self.mission = Mission()
with logged_duration("Loading miz"):
self.mission.load_file(str(miz))
Expand All @@ -105,15 +105,6 @@ def __init__(self, miz: Path, theater: ConflictTheater) -> None:
if self.mission.country(self.RED_COUNTRY.name) is None:
self.mission.coalition["red"].add_country(self.RED_COUNTRY)

def control_point_from_airport(self, airport: Airport) -> ControlPoint:
cp = Airfield(airport, self.theater, starts_blue=airport.is_blue())

# Use the unlimited aircraft option to determine if an airfield should
# be owned by the player when the campaign is "inverted".
cp.captured_invert = airport.unlimited_aircrafts

return cp

def country(self, blue: bool) -> Country:
country = self.mission.country(
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
Expand Down Expand Up @@ -240,36 +231,49 @@ def scenery(self) -> List[SceneryGroup]:

@cached_property
def control_points(self) -> dict[UUID, ControlPoint]:
control_points = {}
control_points: dict[UUID, ControlPoint] = {}
control_point: ControlPoint
for airport in self.mission.terrain.airport_list():
if airport.is_blue() or airport.is_red():
control_point = self.control_point_from_airport(airport)
control_point = self.control_point_builder.create_airfield(airport)
control_points[control_point.id] = control_point

for blue in (False, True):
for group in self.off_map_spawns(blue):
control_point = OffMapSpawn(
str(group.name), group.position, self.theater, starts_blue=blue
control_point = self.control_point_builder.create_off_map(
str(group.name),
group.position,
self.theater,
starts_blue=blue,
captured_invert=group.late_activation,
)
control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point
for ship in self.carriers(blue):
control_point = Carrier(
ship.name, ship.position, self.theater, starts_blue=blue
control_point = self.control_point_builder.create_carrier(
ship.name,
ship.position,
self.theater,
starts_blue=blue,
captured_invert=ship.late_activation,
)
control_point.captured_invert = ship.late_activation
control_points[control_point.id] = control_point
for ship in self.lhas(blue):
control_point = Lha(
ship.name, ship.position, self.theater, starts_blue=blue
control_point = self.control_point_builder.create_lha(
ship.name,
ship.position,
self.theater,
starts_blue=blue,
captured_invert=ship.late_activation,
)
control_point.captured_invert = ship.late_activation
control_points[control_point.id] = control_point
for fob in self.fobs(blue):
control_point = Fob(
str(fob.name), fob.position, self.theater, starts_blue=blue
control_point = self.control_point_builder.create_fob(
str(fob.name),
fob.position,
self.theater,
starts_blue=blue,
captured_invert=fob.late_activation,
)
control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point

return control_points
Expand Down
4 changes: 4 additions & 0 deletions game/squadrons/airwing.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def best_squadrons_for(
for control_point in airfield_cache.operational_airfields:
if control_point.captured != self.player:
continue
if control_point.ferry_only:
continue
capable_at_base = []
for squadron in control_point.squadrons:
if squadron.can_auto_assign_mission(location, task, size, this_turn):
Expand Down Expand Up @@ -91,6 +93,8 @@ def best_available_aircrafts_for(self, task: FlightType) -> list[AircraftType]:
best_aircraft_for_task = AircraftType.priority_list_for_task(task)
for aircraft, squadrons in self.squadrons.items():
for squadron in squadrons:
if squadron.location.ferry_only:
continue
if squadron.untasked_aircraft and squadron.capable_of(task):
aircrafts.append(aircraft)
if aircraft not in best_aircraft_for_task:
Expand Down
1 change: 1 addition & 0 deletions game/theater/controlpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ def __init__(
self.ground_unit_orders = GroundUnitOrders(self)

self.target_position: Optional[Point] = None
self.ferry_only = False

# Initialized late because ControlPoints are constructed before the game is.
self._front_line_db: Database[FrontLine] | None = None
Expand Down
3 changes: 3 additions & 0 deletions game/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,7 @@ def _build_version_string() -> str:
#:
#: Version 10.10
#: * Support for Sinai.
#:
#: Version 10.11
#: * Support for ferry-only bases.
CAMPAIGN_FORMAT_VERSION = (10, 10)
23 changes: 22 additions & 1 deletion qt_ui/windows/basemenu/QBaseMenu2.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import textwrap

from PySide6.QtCore import Qt
from PySide6.QtGui import QCloseEvent, QPixmap
from PySide6.QtWidgets import (
Expand Down Expand Up @@ -60,13 +62,32 @@ def __init__(self, parent, cp: ControlPoint, game_model: GameModel):
pixmap = QPixmap(self.get_base_image())
header.setPixmap(pixmap)

description_layout = QVBoxLayout()
top_layout.addLayout(description_layout)

title = QLabel("<b>" + self.cp.name + "</b>")
title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title")
description_layout.addWidget(title)

if self.cp.ferry_only:
description_layout.addWidget(
QLabel(
"<br />".join(
textwrap.wrap(
"This base only supports ferry missions. Transfer the "
"squadrons to a different base to use them.",
width=80,
)
)
)
)

description_layout.addStretch()

self.intel_summary = QLabel()
self.intel_summary.setToolTip(self.generate_intel_tooltip())
self.update_intel_summary()
top_layout.addWidget(title)
top_layout.addWidget(self.intel_summary)
top_layout.setAlignment(Qt.AlignTop)

Expand Down
9 changes: 7 additions & 2 deletions qt_ui/windows/mission/flight/SquadronSelector.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,13 @@ def update_items(
return

for squadron in self.air_wing.squadrons_for(aircraft):
if squadron.capable_of(task) and squadron.untasked_aircraft:
self.addItem(f"{squadron.location}: {squadron}", squadron)
if not squadron.capable_of(task):
continue
if not squadron.untasked_aircraft:
continue
if squadron.location.ferry_only:
continue
self.addItem(f"{squadron.location}: {squadron}", squadron)

if self.count() == 0:
self.addItem("No capable aircraft available", None)
Expand Down
Loading

0 comments on commit 2344fc0

Please sign in to comment.