mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Add campaign support for ferry-only bases.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3170.
This commit is contained in:
parent
e43874e553
commit
2344fc0b5c
@ -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.
|
||||
|
||||
@ -17,6 +17,7 @@ from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
|
||||
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
|
||||
|
||||
@ -123,7 +124,15 @@ class Campaign:
|
||||
) 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.
|
||||
|
||||
90
game/campaignloader/controlpointbuilder.py
Normal file
90
game/campaignloader/controlpointbuilder.py
Normal file
@ -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
game/campaignloader/controlpointconfig.py
Normal file
21
game/campaignloader/controlpointconfig.py
Normal file
@ -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)
|
||||
@ -12,20 +12,14 @@ from dcs.country import Country
|
||||
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:
|
||||
@ -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))
|
||||
@ -105,15 +105,6 @@ class MizCampaignLoader:
|
||||
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
|
||||
@ -240,36 +231,49 @@ class MizCampaignLoader:
|
||||
|
||||
@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
|
||||
|
||||
@ -53,6 +53,8 @@ class AirWing:
|
||||
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):
|
||||
@ -91,6 +93,8 @@ class AirWing:
|
||||
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:
|
||||
|
||||
@ -356,6 +356,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
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
|
||||
|
||||
@ -185,4 +185,7 @@ VERSION = _build_version_string()
|
||||
#:
|
||||
#: Version 10.10
|
||||
#: * Support for Sinai.
|
||||
#:
|
||||
#: Version 10.11
|
||||
#: * Support for ferry-only bases.
|
||||
CAMPAIGN_FORMAT_VERSION = (10, 10)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import textwrap
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QCloseEvent, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
@ -60,13 +62,32 @@ class QBaseMenu2(QDialog):
|
||||
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)
|
||||
|
||||
|
||||
@ -48,8 +48,13 @@ class SquadronSelector(QComboBox):
|
||||
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)
|
||||
|
||||
@ -4,11 +4,33 @@ theater: Caucasus
|
||||
authors: Starfire
|
||||
recommended_player_faction: USA 2005
|
||||
recommended_enemy_faction: Russia 2010
|
||||
description: <p>United Nations Observer Mission in Georgia (UNOMIG) observers stationed in Georgia to monitor the ceasefire between Georgia and Abkhazia have been cut off from friendly forces by Russian troops backing the separatist state. The UNOMIG HQ at Sukhumi has been taken, and a small contingent of observers and troops at the Zugdidi Sector HQ will have to make a run for the coast, supported by offshore US naval aircraft. The contingent is aware that their best shot at survival is to swiftly retake Sukhumi before Russian forces have a chance to dig in, so that friendly ground forces can land and reinforce them.</p><p><strong>Note:</strong> Ground unit purchase will not be available past Turn 0 until Sukhumi is retaken, so it is imperative you reach Sukhumi with at least one surviving ground unit to capture it. Two Hueys are available at Zugdidi for some close air support. The player can either play the first leg of the scenario as an evacuation with a couple of light vehicles (e.g. Humvees) set on breakthrough (modifying waypoints in the mission editor so they are not charging head-on into enemy ground forces is suggested), or purchase heavier ground units if they wish to experience a more traditional frontline ground war. Once Sukhumi has been captured, squadrons based in Incirlik Turkey can be ferried in via the "From Incirlik" off-map spawn point.</p>
|
||||
description:
|
||||
<p>United Nations Observer Mission in Georgia (UNOMIG) observers stationed in
|
||||
Georgia to monitor the ceasefire between Georgia and Abkhazia have been cut
|
||||
off from friendly forces by Russian troops backing the separatist state. The
|
||||
UNOMIG HQ at Sukhumi has been taken, and a small contingent of observers and
|
||||
troops at the Zugdidi Sector HQ will have to make a run for the coast,
|
||||
supported by offshore US naval aircraft. The contingent is aware that their
|
||||
best shot at survival is to swiftly retake Sukhumi before Russian forces have
|
||||
a chance to dig in, so that friendly ground forces can land and reinforce
|
||||
them.</p><p><strong>Note:</strong> Ground unit purchase will not be available
|
||||
past Turn 0 until Sukhumi is retaken, so it is imperative you reach Sukhumi
|
||||
with at least one surviving ground unit to capture it. Two Hueys are available
|
||||
at Zugdidi for some close air support. The player can either play the first
|
||||
leg of the scenario as an evacuation with a couple of light vehicles (e.g.
|
||||
Humvees) set on breakthrough (modifying waypoints in the mission editor so
|
||||
they are not charging head-on into enemy ground forces is suggested), or
|
||||
purchase heavier ground units if they wish to experience a more traditional
|
||||
frontline ground war. Once Sukhumi has been captured, squadrons based in
|
||||
Incirlik Turkey can be ferried in via the "From Incirlik" off-map spawn
|
||||
point.</p>
|
||||
miz: operation_vectrons_claw.miz
|
||||
performance: 1
|
||||
recommended_start_date: 2008-08-08
|
||||
version: "10.9"
|
||||
version: "10.11"
|
||||
control_points:
|
||||
From Incirlik:
|
||||
ferry_only: true
|
||||
squadrons:
|
||||
Blue CV-1:
|
||||
- primary: BARCAP
|
||||
@ -90,7 +112,7 @@ squadrons:
|
||||
aircraft:
|
||||
- KC-135 Stratotanker
|
||||
size: 1
|
||||
#FARPs
|
||||
#FARPs
|
||||
UNOMIG Sector HQ:
|
||||
- primary: Transport
|
||||
secondary: any
|
||||
@ -103,7 +125,7 @@ squadrons:
|
||||
aircraft:
|
||||
- Mi-24P Hind-F
|
||||
size: 8
|
||||
#Sukhumi-Babushara
|
||||
#Sukhumi-Babushara
|
||||
20:
|
||||
- primary: TARCAP
|
||||
secondary: air-to-air
|
||||
@ -115,7 +137,7 @@ squadrons:
|
||||
aircraft:
|
||||
- Su-25T Frogfoot
|
||||
size: 12
|
||||
#Sochi-Adler
|
||||
#Sochi-Adler
|
||||
18:
|
||||
- primary: Escort
|
||||
secondary: air-to-air
|
||||
@ -132,7 +154,7 @@ squadrons:
|
||||
aircraft:
|
||||
- Su-34 Fullback
|
||||
size: 20
|
||||
#Anapa-Vityazevo
|
||||
#Anapa-Vityazevo
|
||||
12:
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
@ -145,8 +167,8 @@ squadrons:
|
||||
aircraft:
|
||||
- SU-33 Flanker-D
|
||||
size: 18
|
||||
#I am aware there is no Russian LHA. This is just for campaign inversion.
|
||||
#I am aware there is no Russian LHA. This is just for campaign inversion.
|
||||
Red LHA:
|
||||
- primary: BAI
|
||||
secondary: air-to-ground
|
||||
size: 20
|
||||
size: 20
|
||||
|
||||
30
tests/campaignloader/test_controlpointconfig.py
Normal file
30
tests/campaignloader/test_controlpointconfig.py
Normal file
@ -0,0 +1,30 @@
|
||||
from game.campaignloader.controlpointconfig import ControlPointConfig
|
||||
|
||||
|
||||
def test_from_empty_data() -> None:
|
||||
config = ControlPointConfig.from_data({})
|
||||
assert not config.ferry_only
|
||||
|
||||
|
||||
def test_from_data() -> None:
|
||||
config = ControlPointConfig.from_data(
|
||||
{
|
||||
"ferry_only": True,
|
||||
}
|
||||
)
|
||||
assert config.ferry_only
|
||||
|
||||
|
||||
def iter_from_data() -> None:
|
||||
data = dict(
|
||||
ControlPointConfig.iter_from_data(
|
||||
{
|
||||
0: {},
|
||||
"named": {"ferry_only": True},
|
||||
}
|
||||
)
|
||||
)
|
||||
assert data == {
|
||||
0: ControlPointConfig(ferry_only=False),
|
||||
"named": ControlPointConfig(ferry_only=True),
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user