mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Cap squadron size, limit replenishment rate.
This caps squadrons to 12 pilots and limits their replenishment rate to 1 pilot per turn. Should probably make those values configurable, but they aren't currently. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1136
This commit is contained in:
parent
54aa161da0
commit
ace42019fb
@ -4,9 +4,10 @@ Saves from 3.x are not compatible with 4.0.
|
|||||||
|
|
||||||
## Features/Improvements
|
## Features/Improvements
|
||||||
|
|
||||||
* **[Flight Planner]** Added ability to plan Tankers.
|
* **[Campaign]** Squadrons now have a maximum size and killed pilots replenish at a limited rate.
|
||||||
* **[Campaign AI]** AI will plan Tanker flights.
|
* **[Campaign AI]** AI will plan Tanker flights.
|
||||||
* **[Factions]** Added more tankers to factions.
|
* **[Factions]** Added more tankers to factions.
|
||||||
|
* **[Flight Planner]** Added ability to plan Tankers.
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
|
|||||||
@ -327,11 +327,14 @@ class Game:
|
|||||||
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
|
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
|
||||||
self.transfers.perform_transfers()
|
self.transfers.perform_transfers()
|
||||||
|
|
||||||
# Needs to happen *before* planning transfers so we don't cancel the
|
# Needs to happen *before* planning transfers so we don't cancel them.
|
||||||
self.reset_ato()
|
self.reset_ato()
|
||||||
for control_point in self.theater.controlpoints:
|
for control_point in self.theater.controlpoints:
|
||||||
control_point.process_turn(self)
|
control_point.process_turn(self)
|
||||||
|
|
||||||
|
self.blue_air_wing.replenish()
|
||||||
|
self.red_air_wing.replenish()
|
||||||
|
|
||||||
if not skipped and self.turn > 1:
|
if not skipped and self.turn > 1:
|
||||||
for cp in self.theater.player_points():
|
for cp in self.theater.player_points():
|
||||||
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
||||||
|
|||||||
@ -33,6 +33,15 @@ class Settings:
|
|||||||
player_income_multiplier: float = 1.0
|
player_income_multiplier: float = 1.0
|
||||||
enemy_income_multiplier: float = 1.0
|
enemy_income_multiplier: float = 1.0
|
||||||
|
|
||||||
|
#: The maximum number of pilots a squadron can have at one time. Changing this after
|
||||||
|
#: the campaign has started will have no immediate effect; pilots already in the
|
||||||
|
#: squadron will not be removed if the limit is lowered and pilots will not be
|
||||||
|
#: immediately created if the limit is raised.
|
||||||
|
squadron_pilot_limit: int = 12
|
||||||
|
|
||||||
|
#: The number of pilots a squadron can replace per turn.
|
||||||
|
squadron_replenishment_rate: int = 1
|
||||||
|
|
||||||
default_start_type: str = "Cold"
|
default_start_type: str = "Cold"
|
||||||
|
|
||||||
# Mission specific
|
# Mission specific
|
||||||
|
|||||||
@ -80,8 +80,17 @@ class Squadron:
|
|||||||
aircraft: AircraftType
|
aircraft: AircraftType
|
||||||
livery: Optional[str]
|
livery: Optional[str]
|
||||||
mission_types: tuple[FlightType, ...]
|
mission_types: tuple[FlightType, ...]
|
||||||
pilots: list[Pilot]
|
|
||||||
available_pilots: list[Pilot] = field(init=False, hash=False, compare=False)
|
#: The pool of pilots that have not yet been assigned to the squadron. This only
|
||||||
|
#: happens when a preset squadron defines more preset pilots than the squadron limit
|
||||||
|
#: allows. This pool will be consumed before random pilots are generated.
|
||||||
|
pilot_pool: list[Pilot]
|
||||||
|
|
||||||
|
current_roster: list[Pilot] = field(default_factory=list, init=False, hash=False)
|
||||||
|
available_pilots: list[Pilot] = field(
|
||||||
|
default_factory=list, init=False, hash=False, compare=False
|
||||||
|
)
|
||||||
|
|
||||||
auto_assignable_mission_types: set[FlightType] = field(
|
auto_assignable_mission_types: set[FlightType] = field(
|
||||||
init=False, hash=False, compare=False
|
init=False, hash=False, compare=False
|
||||||
)
|
)
|
||||||
@ -93,7 +102,9 @@ class Squadron:
|
|||||||
player: bool
|
player: bool
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
self.available_pilots = list(self.active_pilots)
|
if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
|
||||||
|
raise ValueError("Squadrons can only be created with active pilots.")
|
||||||
|
self._recruit_pilots(self.game.settings.squadron_pilot_limit)
|
||||||
self.auto_assignable_mission_types = set(self.mission_types)
|
self.auto_assignable_mission_types = set(self.mission_types)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -102,11 +113,8 @@ class Squadron:
|
|||||||
return f'{self.name} "{self.nickname}"'
|
return f'{self.name} "{self.nickname}"'
|
||||||
|
|
||||||
def claim_available_pilot(self) -> Optional[Pilot]:
|
def claim_available_pilot(self) -> Optional[Pilot]:
|
||||||
# No pilots available, so the preference is irrelevant. Create a new pilot and
|
|
||||||
# return it.
|
|
||||||
if not self.available_pilots:
|
if not self.available_pilots:
|
||||||
self.enlist_new_pilots(1)
|
return None
|
||||||
return self.available_pilots.pop()
|
|
||||||
|
|
||||||
# For opfor, so player/AI option is irrelevant.
|
# For opfor, so player/AI option is irrelevant.
|
||||||
if not self.player:
|
if not self.player:
|
||||||
@ -127,11 +135,12 @@ class Squadron:
|
|||||||
# No pilot was found that matched the user's preference.
|
# No pilot was found that matched the user's preference.
|
||||||
#
|
#
|
||||||
# If they chose to *never* assign players and only players remain in the pool,
|
# If they chose to *never* assign players and only players remain in the pool,
|
||||||
# we cannot fill the slot with the available pilots. Recruit a new one.
|
# we cannot fill the slot with the available pilots.
|
||||||
#
|
#
|
||||||
# If they prefer players and we're out of players, just return an AI pilot.
|
# If they only *prefer* players and we're out of players, just return an AI
|
||||||
|
# pilot.
|
||||||
if not prefer_players:
|
if not prefer_players:
|
||||||
self.enlist_new_pilots(1)
|
return None
|
||||||
return self.available_pilots.pop()
|
return self.available_pilots.pop()
|
||||||
|
|
||||||
def claim_pilot(self, pilot: Pilot) -> None:
|
def claim_pilot(self, pilot: Pilot) -> None:
|
||||||
@ -151,23 +160,45 @@ class Squadron:
|
|||||||
# repopulating the same size flight from the same squadron.
|
# repopulating the same size flight from the same squadron.
|
||||||
self.available_pilots.extend(reversed(pilots))
|
self.available_pilots.extend(reversed(pilots))
|
||||||
|
|
||||||
def enlist_new_pilots(self, count: int) -> None:
|
def _recruit_pilots(self, count: int) -> None:
|
||||||
new_pilots = [Pilot(self.faker.name()) for _ in range(count)]
|
new_pilots = self.pilot_pool[:count]
|
||||||
self.pilots.extend(new_pilots)
|
self.pilot_pool = self.pilot_pool[count:]
|
||||||
|
count -= len(new_pilots)
|
||||||
|
new_pilots.extend([Pilot(self.faker.name()) for _ in range(count)])
|
||||||
|
self.current_roster.extend(new_pilots)
|
||||||
self.available_pilots.extend(new_pilots)
|
self.available_pilots.extend(new_pilots)
|
||||||
|
|
||||||
|
def replenish_lost_pilots(self) -> None:
|
||||||
|
replenish_count = min(
|
||||||
|
self.game.settings.squadron_replenishment_rate,
|
||||||
|
self.number_of_unfilled_pilot_slots,
|
||||||
|
)
|
||||||
|
if replenish_count > 0:
|
||||||
|
self._recruit_pilots(replenish_count)
|
||||||
|
|
||||||
def return_all_pilots(self) -> None:
|
def return_all_pilots(self) -> None:
|
||||||
self.available_pilots = list(self.active_pilots)
|
self.available_pilots = list(self.active_pilots)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_on_leave(pilot: Pilot) -> None:
|
||||||
|
pilot.send_on_leave()
|
||||||
|
|
||||||
|
def return_from_leave(self, pilot: Pilot):
|
||||||
|
if not self.has_unfilled_pilot_slots:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Cannot return {pilot} from leave because {self} is full"
|
||||||
|
)
|
||||||
|
pilot.return_from_leave()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def faker(self) -> Faker:
|
def faker(self) -> Faker:
|
||||||
return self.game.faker_for(self.player)
|
return self.game.faker_for(self.player)
|
||||||
|
|
||||||
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
|
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
|
||||||
return [p for p in self.pilots if p.status == status]
|
return [p for p in self.current_roster if p.status == status]
|
||||||
|
|
||||||
def _pilots_without_status(self, status: PilotStatus) -> list[Pilot]:
|
def _pilots_without_status(self, status: PilotStatus) -> list[Pilot]:
|
||||||
return [p for p in self.pilots if p.status != status]
|
return [p for p in self.current_roster if p.status != status]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_pilots(self) -> list[Pilot]:
|
def active_pilots(self) -> list[Pilot]:
|
||||||
@ -178,15 +209,30 @@ class Squadron:
|
|||||||
return self._pilots_with_status(PilotStatus.OnLeave)
|
return self._pilots_with_status(PilotStatus.OnLeave)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def number_of_pilots_including_dead(self) -> int:
|
def number_of_pilots_including_inactive(self) -> int:
|
||||||
return len(self.pilots)
|
return len(self.current_roster)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def number_of_living_pilots(self) -> int:
|
def number_of_unfilled_pilot_slots(self) -> int:
|
||||||
return len(self._pilots_without_status(PilotStatus.Dead))
|
return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def number_of_available_pilots(self) -> int:
|
||||||
|
return len(self.available_pilots)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_available_pilots(self) -> bool:
|
||||||
|
return bool(self.available_pilots)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_unfilled_pilot_slots(self) -> bool:
|
||||||
|
return self.number_of_unfilled_pilot_slots > 0
|
||||||
|
|
||||||
|
def can_auto_assign(self, task: FlightType) -> bool:
|
||||||
|
return task in self.auto_assignable_mission_types
|
||||||
|
|
||||||
def pilot_at_index(self, index: int) -> Pilot:
|
def pilot_at_index(self, index: int) -> Pilot:
|
||||||
return self.pilots[index]
|
return self.current_roster[index]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
|
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
|
||||||
@ -223,7 +269,7 @@ class Squadron:
|
|||||||
aircraft=unit_type,
|
aircraft=unit_type,
|
||||||
livery=data.get("livery"),
|
livery=data.get("livery"),
|
||||||
mission_types=tuple(mission_types),
|
mission_types=tuple(mission_types),
|
||||||
pilots=pilots,
|
pilot_pool=pilots,
|
||||||
game=game,
|
game=game,
|
||||||
player=player,
|
player=player,
|
||||||
)
|
)
|
||||||
@ -313,7 +359,7 @@ class AirWing:
|
|||||||
aircraft=aircraft,
|
aircraft=aircraft,
|
||||||
livery=None,
|
livery=None,
|
||||||
mission_types=tuple(tasks_for_aircraft(aircraft)),
|
mission_types=tuple(tasks_for_aircraft(aircraft)),
|
||||||
pilots=[],
|
pilot_pool=[],
|
||||||
game=game,
|
game=game,
|
||||||
player=player,
|
player=player,
|
||||||
)
|
)
|
||||||
@ -327,6 +373,13 @@ class AirWing:
|
|||||||
if task in squadron.mission_types:
|
if task in squadron.mission_types:
|
||||||
yield squadron
|
yield squadron
|
||||||
|
|
||||||
|
def auto_assignable_for_task_with_type(
|
||||||
|
self, aircraft: AircraftType, task: FlightType
|
||||||
|
) -> Iterator[Squadron]:
|
||||||
|
for squadron in self.squadrons_for(aircraft):
|
||||||
|
if squadron.can_auto_assign(task) and squadron.has_available_pilots:
|
||||||
|
yield squadron
|
||||||
|
|
||||||
def squadron_for(self, aircraft: AircraftType) -> Squadron:
|
def squadron_for(self, aircraft: AircraftType) -> Squadron:
|
||||||
return self.squadrons_for(aircraft)[0]
|
return self.squadrons_for(aircraft)[0]
|
||||||
|
|
||||||
@ -336,6 +389,10 @@ 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 replenish(self) -> None:
|
||||||
|
for squadron in self.iter_squadrons():
|
||||||
|
squadron.replenish_lost_pilots()
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
for squadron in self.iter_squadrons():
|
for squadron in self.iter_squadrons():
|
||||||
squadron.return_all_pilots()
|
squadron.return_all_pilots()
|
||||||
|
|||||||
@ -228,37 +228,41 @@ class AirliftPlanner:
|
|||||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||||
self.transfer.position
|
self.transfer.position
|
||||||
)
|
)
|
||||||
|
air_wing = self.game.air_wing_for(self.for_player)
|
||||||
for cp in distance_cache.closest_airfields:
|
for cp in distance_cache.closest_airfields:
|
||||||
if cp.captured != self.for_player:
|
if cp.captured != self.for_player:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||||
for unit_type, available in inventory.all_aircraft:
|
for unit_type, available in inventory.all_aircraft:
|
||||||
squadrons = [
|
squadrons = air_wing.auto_assignable_for_task_with_type(
|
||||||
s
|
unit_type, FlightType.TRANSPORT
|
||||||
for s in self.game.air_wing_for(self.for_player).squadrons_for(
|
)
|
||||||
unit_type
|
for squadron in squadrons:
|
||||||
)
|
if self.compatible_with_mission(unit_type, cp):
|
||||||
if FlightType.TRANSPORT in s.auto_assignable_mission_types
|
while (
|
||||||
]
|
available
|
||||||
if not squadrons:
|
and squadron.has_available_pilots
|
||||||
continue
|
and self.transfer.transport is None
|
||||||
squadron = squadrons[0]
|
):
|
||||||
if self.compatible_with_mission(unit_type, cp):
|
flight_size = self.create_airlift_flight(
|
||||||
while available and self.transfer.transport is None:
|
squadron, inventory
|
||||||
flight_size = self.create_airlift_flight(squadron, inventory)
|
)
|
||||||
available -= flight_size
|
available -= flight_size
|
||||||
if self.package.flights:
|
if self.package.flights:
|
||||||
self.game.ato_for(self.for_player).add_package(self.package)
|
self.game.ato_for(self.for_player).add_package(self.package)
|
||||||
|
|
||||||
def create_airlift_flight(
|
def create_airlift_flight(
|
||||||
self, squadron: Squadron, inventory: ControlPointAircraftInventory
|
self, squadron: Squadron, inventory: ControlPointAircraftInventory
|
||||||
) -> int:
|
) -> int:
|
||||||
available = inventory.available(squadron.aircraft)
|
available_aircraft = inventory.available(squadron.aircraft)
|
||||||
capacity_each = 1 if squadron.aircraft.dcs_unit_type.helicopter else 2
|
capacity_each = 1 if squadron.aircraft.dcs_unit_type.helicopter else 2
|
||||||
required = math.ceil(self.transfer.size / capacity_each)
|
required = math.ceil(self.transfer.size / capacity_each)
|
||||||
flight_size = min(
|
flight_size = min(
|
||||||
required, available, squadron.aircraft.dcs_unit_type.group_size_max
|
required,
|
||||||
|
available_aircraft,
|
||||||
|
squadron.aircraft.dcs_unit_type.group_size_max,
|
||||||
|
squadron.number_of_available_pilots,
|
||||||
)
|
)
|
||||||
capacity = flight_size * capacity_each
|
capacity = flight_size * capacity_each
|
||||||
|
|
||||||
|
|||||||
@ -163,8 +163,6 @@ class AircraftAllocator:
|
|||||||
flight.max_distance
|
flight.max_distance
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prefer using squadrons with pilots first
|
|
||||||
best_understaffed: Optional[Tuple[ControlPoint, Squadron]] = None
|
|
||||||
for airfield in airfields_in_range:
|
for airfield in airfields_in_range:
|
||||||
if not airfield.is_friendly(self.is_player):
|
if not airfield.is_friendly(self.is_player):
|
||||||
continue
|
continue
|
||||||
@ -176,24 +174,14 @@ class AircraftAllocator:
|
|||||||
continue
|
continue
|
||||||
# Valid location with enough aircraft available. Find a squadron to fit
|
# Valid location with enough aircraft available. Find a squadron to fit
|
||||||
# the role.
|
# the role.
|
||||||
for squadron in self.air_wing.squadrons_for(aircraft):
|
squadrons = self.air_wing.auto_assignable_for_task_with_type(
|
||||||
if task not in squadron.auto_assignable_mission_types:
|
aircraft, task
|
||||||
continue
|
)
|
||||||
if len(squadron.available_pilots) >= flight.num_aircraft:
|
for squadron in squadrons:
|
||||||
|
if squadron.number_of_available_pilots >= flight.num_aircraft:
|
||||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||||
return airfield, squadron
|
return airfield, squadron
|
||||||
|
return None
|
||||||
# A compatible squadron that doesn't have enough pilots. Remember it
|
|
||||||
# as a fallback in case we find no better choices.
|
|
||||||
if best_understaffed is None:
|
|
||||||
best_understaffed = airfield, squadron
|
|
||||||
|
|
||||||
if best_understaffed is not None:
|
|
||||||
airfield, squadron = best_understaffed
|
|
||||||
self.global_inventory.for_control_point(airfield).remove_aircraft(
|
|
||||||
squadron.aircraft, flight.num_aircraft
|
|
||||||
)
|
|
||||||
return best_understaffed
|
|
||||||
|
|
||||||
|
|
||||||
class PackageBuilder:
|
class PackageBuilder:
|
||||||
|
|||||||
@ -424,7 +424,7 @@ class SquadronModel(QAbstractListModel):
|
|||||||
self.squadron = squadron
|
self.squadron = squadron
|
||||||
|
|
||||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||||
return self.squadron.number_of_pilots_including_dead
|
return self.squadron.number_of_pilots_including_inactive
|
||||||
|
|
||||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
@ -462,9 +462,9 @@ class SquadronModel(QAbstractListModel):
|
|||||||
pilot = self.pilot_at_index(index)
|
pilot = self.pilot_at_index(index)
|
||||||
self.beginResetModel()
|
self.beginResetModel()
|
||||||
if pilot.on_leave:
|
if pilot.on_leave:
|
||||||
pilot.return_from_leave()
|
self.squadron.return_from_leave(pilot)
|
||||||
else:
|
else:
|
||||||
pilot.send_on_leave()
|
self.squadron.send_on_leave(pilot)
|
||||||
self.endResetModel()
|
self.endResetModel()
|
||||||
|
|
||||||
def is_auto_assignable(self, task: FlightType) -> bool:
|
def is_auto_assignable(self, task: FlightType) -> bool:
|
||||||
|
|||||||
@ -48,10 +48,10 @@ class SquadronDelegate(TwoColumnRowDelegate):
|
|||||||
return self.squadron(index).nickname or ""
|
return self.squadron(index).nickname or ""
|
||||||
elif (row, column) == (1, 1):
|
elif (row, column) == (1, 1):
|
||||||
squadron = self.squadron(index)
|
squadron = self.squadron(index)
|
||||||
alive = squadron.number_of_living_pilots
|
|
||||||
active = len(squadron.active_pilots)
|
active = len(squadron.active_pilots)
|
||||||
available = len(squadron.available_pilots)
|
available = len(squadron.available_pilots)
|
||||||
return f"{alive} pilots, {active} active, {available} unassigned"
|
on_leave = len(squadron.pilots_on_leave)
|
||||||
|
return f"{active} active, {available} unassigned, {on_leave} on leave"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -178,7 +178,9 @@ class SquadronDialog(QDialog):
|
|||||||
if self.check_disabled_button_states(self.toggle_leave_button, index):
|
if self.check_disabled_button_states(self.toggle_leave_button, index):
|
||||||
return
|
return
|
||||||
pilot = self.squadron_model.pilot_at_index(index)
|
pilot = self.squadron_model.pilot_at_index(index)
|
||||||
self.toggle_leave_button.setEnabled(True)
|
self.toggle_leave_button.setEnabled(
|
||||||
|
not pilot.on_leave or self.squadron_model.squadron.has_unfilled_pilot_slots
|
||||||
|
)
|
||||||
self.toggle_leave_button.setText(
|
self.toggle_leave_button.setText(
|
||||||
"Return from leave" if pilot.on_leave else "Send on leave"
|
"Return from leave" if pilot.on_leave else "Send on leave"
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user