Add option to limit squadron sizes and begin full.

Adding temporarily as an option to make sure it's not a terrible idea,
but the old mode will probably go away.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1583.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2808.
This commit is contained in:
Dan Albert 2023-05-05 18:02:23 -07:00
parent 7f94b34277
commit 15fa73a514
15 changed files with 141 additions and 23 deletions

View File

@ -6,6 +6,7 @@ Saves from 6.x are not compatible with 7.0.
* **[Engine]** Support for DCS 2.8.3.37556. * **[Engine]** Support for DCS 2.8.3.37556.
* **[Engine]** Saved games are now a zip file of save assets for easier bug reporting. The new extension is .liberation.zip. Drag and drop that file into bug reports. * **[Engine]** Saved games are now a zip file of save assets for easier bug reporting. The new extension is .liberation.zip. Drag and drop that file into bug reports.
* **[Campaign]** Added options to limit squadron sizes and to begin all squadrons at maximum strength. Maximum squadron size is defined during air wing configuration with default values provided by the campaign.
* **[Campaign AI]** Added an option to instruct the campaign AI to prefer fulfilling missions with squadrons which have a matching primary task. Previously distance from target held a stronger influence than task preference. Primary tasks for squadrons are set by campaign designers but are user-configurable. * **[Campaign AI]** Added an option to instruct the campaign AI to prefer fulfilling missions with squadrons which have a matching primary task. Previously distance from target held a stronger influence than task preference. Primary tasks for squadrons are set by campaign designers but are user-configurable.
* **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation. * **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation.
* **[Mission Generation]** Units on the front line are now hidden on MFDs. * **[Mission Generation]** Units on the front line are now hidden on MFDs.

View File

@ -11,11 +11,15 @@ if TYPE_CHECKING:
from game.theater import ConflictTheater from game.theater import ConflictTheater
DEFAULT_SQUADRON_SIZE = 12
@dataclass(frozen=True) @dataclass(frozen=True)
class SquadronConfig: class SquadronConfig:
primary: FlightType primary: FlightType
secondary: list[FlightType] secondary: list[FlightType]
aircraft: list[str] aircraft: list[str]
max_size: int
name: Optional[str] name: Optional[str]
nickname: Optional[str] nickname: Optional[str]
@ -39,6 +43,7 @@ class SquadronConfig:
FlightType(data["primary"]), FlightType(data["primary"]),
secondary, secondary,
data.get("aircraft", []), data.get("aircraft", []),
data.get("size", DEFAULT_SQUADRON_SIZE),
data.get("name", None), data.get("name", None),
data.get("nickname", None), data.get("nickname", None),
data.get("female_pilot_percentage", None), data.get("female_pilot_percentage", None),

View File

@ -45,6 +45,7 @@ class DefaultSquadronAssigner:
squadron = Squadron.create_from( squadron = Squadron.create_from(
squadron_def, squadron_def,
squadron_config.primary, squadron_config.primary,
squadron_config.max_size,
control_point, control_point,
self.coalition, self.coalition,
self.game, self.game,

View File

@ -158,14 +158,14 @@ class Coalition:
# is handled correctly. # is handled correctly.
self.transfers.perform_transfers() self.transfers.perform_transfers()
def preinit_turn_0(self) -> None: def preinit_turn_0(self, squadrons_start_full: bool) -> None:
"""Runs final Coalition initialization. """Runs final Coalition initialization.
Final initialization occurs before Game.initialize_turn runs for turn 0. Final initialization occurs before Game.initialize_turn runs for turn 0.
""" """
self.air_wing.populate_for_turn_0() self.air_wing.populate_for_turn_0(squadrons_start_full)
def initialize_turn(self) -> None: def initialize_turn(self, is_turn_0: bool) -> None:
"""Processes coalition-specific turn initialization. """Processes coalition-specific turn initialization.
For more information on turn initialization in general, see the documentation For more information on turn initialization in general, see the documentation
@ -184,7 +184,8 @@ class Coalition:
with logged_duration("Transport planning"): with logged_duration("Transport planning"):
self.transfers.plan_transports(self.game.conditions.start_time) self.transfers.plan_transports(self.game.conditions.start_time)
self.plan_missions(self.game.conditions.start_time) if not is_turn_0 or not self.game.settings.enable_squadron_aircraft_limits:
self.plan_missions(self.game.conditions.start_time)
self.plan_procurement() self.plan_procurement()
def refund_outstanding_orders(self) -> None: def refund_outstanding_orders(self) -> None:

View File

@ -296,7 +296,7 @@ class Game:
if self.turn > 1: if self.turn > 1:
self.conditions = self.generate_conditions() self.conditions = self.generate_conditions()
def begin_turn_0(self) -> None: def begin_turn_0(self, squadrons_start_full: bool) -> None:
"""Initialization for the first turn of the game.""" """Initialization for the first turn of the game."""
from .sim import GameUpdateEvents from .sim import GameUpdateEvents
@ -321,8 +321,9 @@ class Game:
# Rotate the whole TGO with the new heading # Rotate the whole TGO with the new heading
tgo.rotate(heading or tgo.heading) tgo.rotate(heading or tgo.heading)
self.blue.preinit_turn_0() self.blue.preinit_turn_0(squadrons_start_full)
self.red.preinit_turn_0() self.red.preinit_turn_0(squadrons_start_full)
# TODO: Check for overfull bases.
# We don't need to actually stream events for turn zero because we haven't given # We don't need to actually stream events for turn zero because we haven't given
# *any* state to the UI yet, so it will need to do a full draw once we do. # *any* state to the UI yet, so it will need to do a full draw once we do.
self.initialize_turn(GameUpdateEvents()) self.initialize_turn(GameUpdateEvents())
@ -381,7 +382,10 @@ class Game:
self.red.bullseye = Bullseye(player_cp.position) self.red.bullseye = Bullseye(player_cp.position)
def initialize_turn( def initialize_turn(
self, events: GameUpdateEvents, for_red: bool = True, for_blue: bool = True self,
events: GameUpdateEvents,
for_red: bool = True,
for_blue: bool = True,
) -> None: ) -> None:
"""Performs turn initialization for the specified players. """Performs turn initialization for the specified players.
@ -433,9 +437,9 @@ class Game:
# Plan Coalition specific turn # Plan Coalition specific turn
if for_blue: if for_blue:
self.blue.initialize_turn() self.blue.initialize_turn(self.turn == 0)
if for_red: if for_red:
self.red.initialize_turn() self.red.initialize_turn(self.turn == 0)
# Update cull zones # Update cull zones
with logged_duration("Computing culling positions"): with logged_duration("Computing culling positions"):

View File

@ -235,6 +235,8 @@ class ProcurementAi:
): ):
if not squadron.can_provide_pilots(request.number): if not squadron.can_provide_pilots(request.number):
continue continue
if not squadron.has_aircraft_capacity_for(request.number):
continue
if squadron.location.unclaimed_parking() < request.number: if squadron.location.unclaimed_parking() < request.number:
continue continue
if self.threat_zones.threatened(squadron.location.position): if self.threat_zones.threatened(squadron.location.position):

View File

@ -109,7 +109,11 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
return item.owned_aircraft return item.owned_aircraft
def can_buy(self, item: Squadron) -> bool: def can_buy(self, item: Squadron) -> bool:
return super().can_buy(item) and self.control_point.unclaimed_parking() > 0 return (
super().can_buy(item)
and self.control_point.unclaimed_parking() > 0
and item.has_aircraft_capacity_for(1)
)
def can_sell(self, item: Squadron) -> bool: def can_sell(self, item: Squadron) -> bool:
return item.untasked_aircraft > 0 return item.untasked_aircraft > 0

View File

@ -264,6 +264,15 @@ class Settings:
"this many pilots each turn up to the limit." "this many pilots each turn up to the limit."
), ),
) )
# Feature flag for squadron limits.
enable_squadron_aircraft_limits: bool = boolean_option(
"Enable per-squadron aircraft limits",
CAMPAIGN_MANAGEMENT_PAGE,
PILOTS_AND_SQUADRONS_SECTION,
default=False,
remember_player_choice=True,
detail="If set, squadrons will be limited to a maximum number of aircraft.",
)
# HQ Automation # HQ Automation
automate_runway_repair: bool = boolean_option( automate_runway_repair: bool = boolean_option(

View File

@ -125,9 +125,9 @@ class AirWing:
def squadron_at_index(self, index: int) -> Squadron: def squadron_at_index(self, index: int) -> Squadron:
return list(self.iter_squadrons())[index] return list(self.iter_squadrons())[index]
def populate_for_turn_0(self) -> None: def populate_for_turn_0(self, squadrons_start_full: bool) -> None:
for squadron in self.iter_squadrons(): for squadron in self.iter_squadrons():
squadron.populate_for_turn_0() squadron.populate_for_turn_0(squadrons_start_full)
def end_turn(self) -> None: def end_turn(self) -> None:
for squadron in self.iter_squadrons(): for squadron in self.iter_squadrons():

View File

@ -31,6 +31,7 @@ class Squadron:
country: str country: str
role: str role: str
aircraft: AircraftType aircraft: AircraftType
max_size: int
livery: Optional[str] livery: Optional[str]
primary_task: FlightType primary_task: FlightType
auto_assignable_mission_types: set[FlightType] auto_assignable_mission_types: set[FlightType]
@ -161,10 +162,12 @@ class Squadron:
self.current_roster.extend(new_pilots) self.current_roster.extend(new_pilots)
self.available_pilots.extend(new_pilots) self.available_pilots.extend(new_pilots)
def populate_for_turn_0(self) -> None: def populate_for_turn_0(self, squadrons_start_full: bool) -> None:
if any(p.status is not PilotStatus.Active for p in self.pilot_pool): if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
raise ValueError("Squadrons can only be created with active pilots.") raise ValueError("Squadrons can only be created with active pilots.")
self._recruit_pilots(self.settings.squadron_pilot_limit) self._recruit_pilots(self.settings.squadron_pilot_limit)
if squadrons_start_full:
self.owned_aircraft = self.max_size
def end_turn(self) -> None: def end_turn(self) -> None:
if self.destination is not None: if self.destination is not None:
@ -202,7 +205,7 @@ class Squadron:
return [p for p in self.current_roster if p.status != status] return [p for p in self.current_roster if p.status != status]
@property @property
def max_size(self) -> int: def pilot_limit(self) -> int:
return self.settings.squadron_pilot_limit return self.settings.squadron_pilot_limit
@property @property
@ -230,7 +233,7 @@ class Squadron:
@property @property
def _number_of_unfilled_pilot_slots(self) -> int: def _number_of_unfilled_pilot_slots(self) -> int:
return self.max_size - len(self.active_pilots) return self.pilot_limit - len(self.active_pilots)
@property @property
def number_of_available_pilots(self) -> int: def number_of_available_pilots(self) -> int:
@ -334,6 +337,12 @@ class Squadron:
def expected_size_next_turn(self) -> int: def expected_size_next_turn(self) -> int:
return self.owned_aircraft + self.pending_deliveries return self.owned_aircraft + self.pending_deliveries
def has_aircraft_capacity_for(self, n: int) -> bool:
if not self.settings.enable_squadron_aircraft_limits:
return True
remaining = self.max_size - self.owned_aircraft - self.pending_deliveries
return remaining >= n
@property @property
def arrival(self) -> ControlPoint: def arrival(self) -> ControlPoint:
return self.location if self.destination is None else self.destination return self.location if self.destination is None else self.destination
@ -424,6 +433,7 @@ class Squadron:
cls, cls,
squadron_def: SquadronDef, squadron_def: SquadronDef,
primary_task: FlightType, primary_task: FlightType,
max_size: int,
base: ControlPoint, base: ControlPoint,
coalition: Coalition, coalition: Coalition,
game: Game, game: Game,
@ -435,6 +445,7 @@ class Squadron:
squadron_def.country, squadron_def.country,
squadron_def.role, squadron_def.role,
squadron_def.aircraft, squadron_def.aircraft,
max_size,
squadron_def.livery, squadron_def.livery,
primary_task, primary_task,
squadron_def.auto_assignable_mission_types, squadron_def.auto_assignable_mission_types,

View File

@ -172,4 +172,7 @@ VERSION = _build_version_string()
#: #:
#: Version 10.6 #: Version 10.6
#: * Support in-line definitions of campaign-specific factions. #: * Support in-line definitions of campaign-specific factions.
CAMPAIGN_FORMAT_VERSION = (10, 6) #:
#: Version 10.7
#: * Support for defining squadron sizes.
CAMPAIGN_FORMAT_VERSION = (10, 7)

View File

@ -221,6 +221,15 @@ def parse_args() -> argparse.Namespace:
"--auto-procurement", action="store_true", help="Automate bluefor procurement." "--auto-procurement", action="store_true", help="Automate bluefor procurement."
) )
new_game.add_argument(
"--use-new-squadron-rules",
action="store_true",
help=(
"Limit the number of aircraft per squadron and begin the campaign with "
"them at full strength."
),
)
new_game.add_argument( new_game.add_argument(
"--inverted", action="store_true", help="Invert the campaign." "--inverted", action="store_true", help="Invert the campaign."
) )
@ -263,6 +272,7 @@ def create_game(
start_date: datetime, start_date: datetime,
restrict_weapons_by_date: bool, restrict_weapons_by_date: bool,
advanced_iads: bool, advanced_iads: bool,
use_new_squadron_rules: bool,
) -> Game: ) -> Game:
first_start = liberation_install.init() first_start = liberation_install.init()
if first_start: if first_start:
@ -297,6 +307,7 @@ def create_game(
enable_frontline_cheats=cheats, enable_frontline_cheats=cheats,
enable_base_capture_cheat=cheats, enable_base_capture_cheat=cheats,
restrict_weapons_by_date=restrict_weapons_by_date, restrict_weapons_by_date=restrict_weapons_by_date,
enable_squadron_aircraft_limits=use_new_squadron_rules,
), ),
GeneratorSettings( GeneratorSettings(
start_date=start_date, start_date=start_date,
@ -323,7 +334,7 @@ def create_game(
lua_plugin_manager, lua_plugin_manager,
) )
game = generator.generate() game = generator.generate()
game.begin_turn_0() game.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
return game return game
@ -417,6 +428,7 @@ def main():
args.date, args.date,
args.restrict_weapons_by_date, args.restrict_weapons_by_date,
args.advanced_iads, args.advanced_iads,
args.use_new_squadron_rules,
) )
if args.subcommand == "lint-weapons": if args.subcommand == "lint-weapons":
lint_weapon_data_for_aircraft(AircraftType.named(args.aircraft)) lint_weapon_data_for_aircraft(AircraftType.named(args.aircraft))

View File

@ -27,10 +27,12 @@ from PySide6.QtWidgets import (
QToolButton, QToolButton,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
QSpinBox,
) )
from game import Game from game import Game
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.campaignloader.campaignairwingconfig import DEFAULT_SQUADRON_SIZE
from game.coalition import Coalition from game.coalition import Coalition
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.squadrons import AirWing, Pilot, Squadron from game.squadrons import AirWing, Pilot, Squadron
@ -127,6 +129,26 @@ class SquadronBaseSelector(QComboBox):
self.update() self.update()
class SquadronSizeSpinner(QSpinBox):
def __init__(self, starting_size: int, parent: QWidget | None) -> None:
super().__init__(parent)
# Disable text editing, which wouldn't work in the first place, but also
# obnoxiously selects the text on change (highlighting it) and leaves a flashing
# cursor in the middle of the element when clicked.
self.lineEdit().setEnabled(False)
self.setMinimum(1)
self.setValue(starting_size)
# def sizeHint(self) -> QSize:
# # The default size hinting fails to deal with label width, and will truncate
# # "Paused".
# size = super().sizeHint()
# size.setWidth(86)
# return size
class SquadronConfigurationBox(QGroupBox): class SquadronConfigurationBox(QGroupBox):
remove_squadron_signal = Signal(Squadron) remove_squadron_signal = Signal(Squadron)
@ -166,9 +188,20 @@ class SquadronConfigurationBox(QGroupBox):
reroll_nickname_button.clicked.connect(self.reroll_nickname) reroll_nickname_button.clicked.connect(self.reroll_nickname)
nickname_edit_layout.addWidget(reroll_nickname_button, 1, 1, Qt.AlignTop) nickname_edit_layout.addWidget(reroll_nickname_button, 1, 1, Qt.AlignTop)
left_column.addWidget(QLabel("Primary task:")) task_and_size_row = QHBoxLayout()
left_column.addLayout(task_and_size_row)
size_column = QVBoxLayout()
left_column.addLayout(size_column)
size_column.addWidget(QLabel("Max size:"))
self.max_size_selector = SquadronSizeSpinner(self.squadron.max_size, self)
size_column.addWidget(self.max_size_selector)
task_column = QVBoxLayout()
left_column.addLayout(task_column)
task_column.addWidget(QLabel("Primary task:"))
self.primary_task_selector = PrimaryTaskSelector.for_squadron(self.squadron) self.primary_task_selector = PrimaryTaskSelector.for_squadron(self.squadron)
left_column.addWidget(self.primary_task_selector) task_column.addWidget(self.primary_task_selector)
left_column.addWidget(QLabel("Base:")) left_column.addWidget(QLabel("Base:"))
self.base_selector = SquadronBaseSelector( self.base_selector = SquadronBaseSelector(
@ -222,6 +255,7 @@ class SquadronConfigurationBox(QGroupBox):
self.name_edit.setText(self.squadron.name) self.name_edit.setText(self.squadron.name)
self.nickname_edit.setText(self.squadron.nickname) self.nickname_edit.setText(self.squadron.nickname)
self.primary_task_selector.setCurrentText(self.squadron.primary_task.value) self.primary_task_selector.setCurrentText(self.squadron.primary_task.value)
self.max_size_selector.setValue(self.squadron.max_size)
self.base_selector.setCurrentText(self.squadron.location.name) self.base_selector.setCurrentText(self.squadron.location.name)
self.player_list.setText( self.player_list.setText(
"<br />".join(p.name for p in self.claim_players_from_squadron()) "<br />".join(p.name for p in self.claim_players_from_squadron())
@ -246,6 +280,7 @@ class SquadronConfigurationBox(QGroupBox):
squadron = Squadron.create_from( squadron = Squadron.create_from(
selected_def, selected_def,
self.squadron.primary_task, self.squadron.primary_task,
self.squadron.max_size,
self.squadron.location, self.squadron.location,
self.coalition, self.coalition,
self.game, self.game,
@ -292,6 +327,7 @@ class SquadronConfigurationBox(QGroupBox):
def apply(self) -> Squadron: def apply(self) -> Squadron:
self.squadron.name = self.name_edit.text() self.squadron.name = self.name_edit.text()
self.squadron.nickname = self.nickname_edit.text() self.squadron.nickname = self.nickname_edit.text()
self.squadron.max_size = self.max_size_selector.value()
if (primary_task := self.primary_task_selector.selected_task) is not None: if (primary_task := self.primary_task_selector.selected_task) is not None:
self.squadron.primary_task = primary_task self.squadron.primary_task = primary_task
else: else:
@ -557,7 +593,12 @@ class AirWingConfigurationTab(QWidget):
) )
squadron = Squadron.create_from( squadron = Squadron.create_from(
squadron_def, selected_task, selected_base, self.coalition, self.game squadron_def,
selected_task,
DEFAULT_SQUADRON_SIZE,
selected_base,
self.coalition,
self.game,
) )
# Add Squadron # Add Squadron

View File

@ -155,6 +155,7 @@ class NewGameWizard(QtWidgets.QWizard):
self.lua_plugin_manager.save_player_settings() self.lua_plugin_manager.save_player_settings()
use_new_squadron_rules = self.field("use_new_squadron_rules")
logging.info("New campaign start date: %s", start_date.strftime("%m/%d/%Y")) logging.info("New campaign start date: %s", start_date.strftime("%m/%d/%Y"))
settings = Settings( settings = Settings(
player_income_multiplier=self.field("player_income_multiplier") / 10, player_income_multiplier=self.field("player_income_multiplier") / 10,
@ -168,6 +169,7 @@ class NewGameWizard(QtWidgets.QWizard):
), ),
automate_aircraft_reinforcements=self.field("automate_aircraft_purchases"), automate_aircraft_reinforcements=self.field("automate_aircraft_purchases"),
supercarrier=self.field("supercarrier"), supercarrier=self.field("supercarrier"),
enable_squadron_aircraft_limits=use_new_squadron_rules,
) )
settings.save_player_settings() settings.save_player_settings()
generator_settings = GeneratorSettings( generator_settings = GeneratorSettings(
@ -223,7 +225,7 @@ class NewGameWizard(QtWidgets.QWizard):
AirWingConfigurationDialog(self.generatedGame, self).exec_() AirWingConfigurationDialog(self.generatedGame, self).exec_()
self.generatedGame.begin_turn_0() self.generatedGame.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
super(NewGameWizard, self).accept() super(NewGameWizard, self).accept()
@ -603,6 +605,17 @@ class DifficultyAndAutomationOptions(QtWidgets.QWizardPage):
self.registerField("enemy_starting_money", self.enemy_budget.starting_money) self.registerField("enemy_starting_money", self.enemy_budget.starting_money)
economy_layout.addLayout(self.enemy_budget) economy_layout.addLayout(self.enemy_budget)
new_squadron_rules = QtWidgets.QCheckBox("Enable new squadron rules")
new_squadron_rules.setChecked(default_settings.enable_squadron_aircraft_limits)
self.registerField("use_new_squadron_rules", new_squadron_rules)
economy_layout.addWidget(new_squadron_rules)
economy_layout.addWidget(
QLabel(
"With new squadron rules enabled, squadrons will not be able to exceed a maximum number of aircraft "
"(configurable), and the campaign will begin with all squadrons at full strength."
)
)
assist_group = QtWidgets.QGroupBox("Player assists") assist_group = QtWidgets.QGroupBox("Player assists")
layout.addWidget(assist_group) layout.addWidget(assist_group)
assist_layout = QtWidgets.QGridLayout() assist_layout = QtWidgets.QGridLayout()

View File

@ -9,7 +9,7 @@ recommended_enemy_faction: Russia 2010
recommended_start_date: 2004-01-07 recommended_start_date: 2004-01-07
miz: black_sea.miz miz: black_sea.miz
performance: 2 performance: 2
version: "10.2" version: "10.7"
squadrons: squadrons:
# Anapa-Vityazevo # Anapa-Vityazevo
12: 12:
@ -20,16 +20,20 @@ squadrons:
- primary: AEW&C - primary: AEW&C
aircraft: aircraft:
- A-50 - A-50
size: 2
- primary: Refueling - primary: Refueling
aircraft: aircraft:
- IL-78M - IL-78M
size: 2
- primary: Transport - primary: Transport
aircraft: aircraft:
- IL-78MD - IL-78MD
size: 4
- primary: Strike - primary: Strike
secondary: air-to-ground secondary: air-to-ground
aircraft: aircraft:
- Tu-160 Blackjack - Tu-160 Blackjack
size: 4
# Krasnodar-Center # Krasnodar-Center
13: 13:
- primary: BARCAP - primary: BARCAP
@ -77,6 +81,7 @@ squadrons:
- primary: Transport - primary: Transport
aircraft: aircraft:
- UH-60A - UH-60A
size: 8
# Kobuleti # Kobuleti
24: 24:
- primary: BARCAP - primary: BARCAP
@ -99,16 +104,20 @@ squadrons:
- primary: AEW&C - primary: AEW&C
aircraft: aircraft:
- E-3A - E-3A
size: 2
- primary: Refueling - primary: Refueling
aircraft: aircraft:
- KC-135 Stratotanker - KC-135 Stratotanker
size: 2
- primary: Transport - primary: Transport
aircraft: aircraft:
- C-17A - C-17A
size: 4
- primary: Strike - primary: Strike
secondary: air-to-ground secondary: air-to-ground
aircraft: aircraft:
- B-1B Lancer - B-1B Lancer
size: 4
Blue CV: Blue CV:
- primary: BARCAP - primary: BARCAP
secondary: air-to-air secondary: air-to-air
@ -129,6 +138,7 @@ squadrons:
- primary: Refueling - primary: Refueling
aircraft: aircraft:
- S-3B Tanker - S-3B Tanker
size: 2
Blue LHA: Blue LHA:
- primary: BAI - primary: BAI
secondary: air-to-ground secondary: air-to-ground
@ -148,6 +158,7 @@ squadrons:
- primary: BAI - primary: BAI
secondary: any secondary: any
- primary: Refueling - primary: Refueling
size: 2
Red LHA: Red LHA:
- primary: BAI - primary: BAI
secondary: air-to-ground secondary: air-to-ground