mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Assign aircraft to squadrons rather than bases.
This is needed to support the upcoming squadron transfers, since squadrons need to bring their aircraft with them. https://github.com/dcs-liberation/dcs_liberation/issues/1145
This commit is contained in:
parent
99274133ff
commit
4423288a53
@ -8,6 +8,7 @@ Saves from 4.x are not compatible with 5.0.
|
|||||||
* **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated.
|
* **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated.
|
||||||
* **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts.
|
* **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts.
|
||||||
* **[Campaign]** (WIP) Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/issues/1145 for status.
|
* **[Campaign]** (WIP) Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/issues/1145 for status.
|
||||||
|
* **[Campaign]** Aircraft now belong to squadrons rather than bases to support squadron location transfers.
|
||||||
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.
|
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.
|
||||||
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
|
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
|
||||||
* **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points.
|
* **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points.
|
||||||
|
|||||||
@ -10,7 +10,6 @@ from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
|
|||||||
from game.commander import TheaterCommander
|
from game.commander import TheaterCommander
|
||||||
from game.commander.missionscheduler import MissionScheduler
|
from game.commander.missionscheduler import MissionScheduler
|
||||||
from game.income import Income
|
from game.income import Income
|
||||||
from game.inventory import GlobalAircraftInventory
|
|
||||||
from game.navmesh import NavMesh
|
from game.navmesh import NavMesh
|
||||||
from game.orderedset import OrderedSet
|
from game.orderedset import OrderedSet
|
||||||
from game.profiling import logged_duration, MultiEventTracer
|
from game.profiling import logged_duration, MultiEventTracer
|
||||||
@ -88,10 +87,6 @@ class Coalition:
|
|||||||
assert self._navmesh is not None
|
assert self._navmesh is not None
|
||||||
return self._navmesh
|
return self._navmesh
|
||||||
|
|
||||||
@property
|
|
||||||
def aircraft_inventory(self) -> GlobalAircraftInventory:
|
|
||||||
return self.game.aircraft_inventory
|
|
||||||
|
|
||||||
def __getstate__(self) -> dict[str, Any]:
|
def __getstate__(self) -> dict[str, Any]:
|
||||||
state = self.__dict__.copy()
|
state = self.__dict__.copy()
|
||||||
# Avoid persisting any volatile types that can be deterministically
|
# Avoid persisting any volatile types that can be deterministically
|
||||||
@ -196,7 +191,9 @@ class Coalition:
|
|||||||
return
|
return
|
||||||
|
|
||||||
for cp in self.game.theater.control_points_for(self.player):
|
for cp in self.game.theater.control_points_for(self.player):
|
||||||
cp.pending_unit_deliveries.refund_all(self)
|
cp.ground_unit_orders.refund_all(self)
|
||||||
|
for squadron in self.air_wing.iter_squadrons():
|
||||||
|
squadron.refund_orders()
|
||||||
|
|
||||||
def plan_missions(self) -> None:
|
def plan_missions(self) -> None:
|
||||||
color = "Blue" if self.player else "Red"
|
color = "Blue" if self.player else "Red"
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from game.commander.missionproposals import ProposedFlight
|
from game.commander.missionproposals import ProposedFlight
|
||||||
from game.inventory import GlobalAircraftInventory
|
|
||||||
from game.squadrons.squadron import Squadron
|
|
||||||
from game.squadrons.airwing import AirWing
|
from game.squadrons.airwing import AirWing
|
||||||
|
from game.squadrons.squadron import Squadron
|
||||||
from game.theater import ControlPoint, MissionTarget
|
from game.theater import ControlPoint, MissionTarget
|
||||||
from game.utils import meters
|
from game.utils import meters
|
||||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||||
@ -15,15 +14,10 @@ class AircraftAllocator:
|
|||||||
"""Finds suitable aircraft for proposed missions."""
|
"""Finds suitable aircraft for proposed missions."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, air_wing: AirWing, closest_airfields: ClosestAirfields, is_player: bool
|
||||||
air_wing: AirWing,
|
|
||||||
closest_airfields: ClosestAirfields,
|
|
||||||
global_inventory: GlobalAircraftInventory,
|
|
||||||
is_player: bool,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.air_wing = air_wing
|
self.air_wing = air_wing
|
||||||
self.closest_airfields = closest_airfields
|
self.closest_airfields = closest_airfields
|
||||||
self.global_inventory = global_inventory
|
|
||||||
self.is_player = is_player
|
self.is_player = is_player
|
||||||
|
|
||||||
def find_squadron_for_flight(
|
def find_squadron_for_flight(
|
||||||
@ -56,12 +50,9 @@ class AircraftAllocator:
|
|||||||
for airfield in self.closest_airfields.operational_airfields:
|
for airfield in self.closest_airfields.operational_airfields:
|
||||||
if not airfield.is_friendly(self.is_player):
|
if not airfield.is_friendly(self.is_player):
|
||||||
continue
|
continue
|
||||||
inventory = self.global_inventory.for_control_point(airfield)
|
|
||||||
for aircraft in types:
|
for aircraft in types:
|
||||||
if not airfield.can_operate(aircraft):
|
if not airfield.can_operate(aircraft):
|
||||||
continue
|
continue
|
||||||
if inventory.available(aircraft) < flight.num_aircraft:
|
|
||||||
continue
|
|
||||||
distance_to_target = meters(target.distance_to(airfield))
|
distance_to_target = meters(target.distance_to(airfield))
|
||||||
if distance_to_target > aircraft.max_mission_range:
|
if distance_to_target > aircraft.max_mission_range:
|
||||||
continue
|
continue
|
||||||
@ -71,9 +62,8 @@ class AircraftAllocator:
|
|||||||
aircraft, task, airfield
|
aircraft, task, airfield
|
||||||
)
|
)
|
||||||
for squadron in squadrons:
|
for squadron in squadrons:
|
||||||
if squadron.operates_from(airfield) and squadron.can_provide_pilots(
|
if squadron.operates_from(airfield) and squadron.can_fulfill_flight(
|
||||||
flight.num_aircraft
|
flight.num_aircraft
|
||||||
):
|
):
|
||||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
|
||||||
return airfield, squadron
|
return airfield, squadron
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -157,7 +157,10 @@ class ObjectiveFinder:
|
|||||||
for control_point in self.enemy_control_points():
|
for control_point in self.enemy_control_points():
|
||||||
if not isinstance(control_point, Airfield):
|
if not isinstance(control_point, Airfield):
|
||||||
continue
|
continue
|
||||||
if control_point.base.total_aircraft >= min_aircraft:
|
if (
|
||||||
|
control_point.allocated_aircraft(self.game).total_present
|
||||||
|
>= min_aircraft
|
||||||
|
):
|
||||||
airfields.append(control_point)
|
airfields.append(control_point)
|
||||||
return self._targets_by_range(airfields)
|
return self._targets_by_range(airfields)
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from game.commander.aircraftallocator import AircraftAllocator
|
||||||
from game.commander.missionproposals import ProposedFlight
|
from game.commander.missionproposals import ProposedFlight
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.inventory import GlobalAircraftInventory
|
|
||||||
from game.squadrons.airwing import AirWing
|
from game.squadrons.airwing import AirWing
|
||||||
from game.theater import MissionTarget, OffMapSpawn, ControlPoint
|
from game.theater import MissionTarget, OffMapSpawn, ControlPoint
|
||||||
from game.utils import nautical_miles
|
from game.utils import nautical_miles
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from game.commander.aircraftallocator import AircraftAllocator
|
|
||||||
from gen.flights.closestairfields import ClosestAirfields
|
from gen.flights.closestairfields import ClosestAirfields
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
|
|
||||||
@ -19,7 +18,6 @@ class PackageBuilder:
|
|||||||
self,
|
self,
|
||||||
location: MissionTarget,
|
location: MissionTarget,
|
||||||
closest_airfields: ClosestAirfields,
|
closest_airfields: ClosestAirfields,
|
||||||
global_inventory: GlobalAircraftInventory,
|
|
||||||
air_wing: AirWing,
|
air_wing: AirWing,
|
||||||
is_player: bool,
|
is_player: bool,
|
||||||
package_country: str,
|
package_country: str,
|
||||||
@ -30,10 +28,7 @@ class PackageBuilder:
|
|||||||
self.is_player = is_player
|
self.is_player = is_player
|
||||||
self.package_country = package_country
|
self.package_country = package_country
|
||||||
self.package = Package(location, auto_asap=asap)
|
self.package = Package(location, auto_asap=asap)
|
||||||
self.allocator = AircraftAllocator(
|
self.allocator = AircraftAllocator(air_wing, closest_airfields, is_player)
|
||||||
air_wing, closest_airfields, global_inventory, is_player
|
|
||||||
)
|
|
||||||
self.global_inventory = global_inventory
|
|
||||||
self.start_type = start_type
|
self.start_type = start_type
|
||||||
|
|
||||||
def plan_flight(self, plan: ProposedFlight) -> bool:
|
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||||
@ -93,6 +88,5 @@ class PackageBuilder:
|
|||||||
"""Returns any planned flights to the inventory."""
|
"""Returns any planned flights to the inventory."""
|
||||||
flights = list(self.package.flights)
|
flights = list(self.package.flights)
|
||||||
for flight in flights:
|
for flight in flights:
|
||||||
self.global_inventory.return_from_flight(flight)
|
flight.return_pilots_and_aircraft()
|
||||||
flight.clear_roster()
|
|
||||||
self.package.remove_flight(flight)
|
self.package.remove_flight(flight)
|
||||||
|
|||||||
@ -5,15 +5,14 @@ from collections import defaultdict
|
|||||||
from typing import Set, Iterable, Dict, TYPE_CHECKING, Optional
|
from typing import Set, Iterable, Dict, TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from game.commander.missionproposals import ProposedMission, ProposedFlight, EscortType
|
from game.commander.missionproposals import ProposedMission, ProposedFlight, EscortType
|
||||||
|
from game.commander.packagebuilder import PackageBuilder
|
||||||
from game.data.doctrine import Doctrine
|
from game.data.doctrine import Doctrine
|
||||||
from game.inventory import GlobalAircraftInventory
|
|
||||||
from game.procurement import AircraftProcurementRequest
|
from game.procurement import AircraftProcurementRequest
|
||||||
from game.profiling import MultiEventTracer
|
from game.profiling import MultiEventTracer
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
from game.squadrons import AirWing
|
from game.squadrons import AirWing
|
||||||
from game.theater import ConflictTheater
|
from game.theater import ConflictTheater
|
||||||
from game.threatzones import ThreatZones
|
from game.threatzones import ThreatZones
|
||||||
from game.commander.packagebuilder import PackageBuilder
|
|
||||||
from gen.ato import AirTaskingOrder, Package
|
from gen.ato import AirTaskingOrder, Package
|
||||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
@ -27,15 +26,10 @@ class PackageFulfiller:
|
|||||||
"""Responsible for package aircraft allocation and flight plan layout."""
|
"""Responsible for package aircraft allocation and flight plan layout."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, coalition: Coalition, theater: ConflictTheater, settings: Settings
|
||||||
coalition: Coalition,
|
|
||||||
theater: ConflictTheater,
|
|
||||||
aircraft_inventory: GlobalAircraftInventory,
|
|
||||||
settings: Settings,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.coalition = coalition
|
self.coalition = coalition
|
||||||
self.theater = theater
|
self.theater = theater
|
||||||
self.aircraft_inventory = aircraft_inventory
|
|
||||||
self.player_missions_asap = settings.auto_ato_player_missions_asap
|
self.player_missions_asap = settings.auto_ato_player_missions_asap
|
||||||
self.default_start_type = settings.default_start_type
|
self.default_start_type = settings.default_start_type
|
||||||
|
|
||||||
@ -137,7 +131,6 @@ class PackageFulfiller:
|
|||||||
builder = PackageBuilder(
|
builder = PackageBuilder(
|
||||||
mission.location,
|
mission.location,
|
||||||
ObjectiveDistanceCache.get_closest_airfields(mission.location),
|
ObjectiveDistanceCache.get_closest_airfields(mission.location),
|
||||||
self.aircraft_inventory,
|
|
||||||
self.air_wing,
|
self.air_wing,
|
||||||
self.is_player,
|
self.is_player,
|
||||||
self.coalition.country_name,
|
self.coalition.country_name,
|
||||||
|
|||||||
@ -53,8 +53,6 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
def execute(self, coalition: Coalition) -> None:
|
def execute(self, coalition: Coalition) -> None:
|
||||||
if self.package is None:
|
if self.package is None:
|
||||||
raise RuntimeError("Attempted to execute failed package planning task")
|
raise RuntimeError("Attempted to execute failed package planning task")
|
||||||
for flight in self.package.flights:
|
|
||||||
coalition.aircraft_inventory.claim_for_flight(flight)
|
|
||||||
coalition.ato.add_package(self.package)
|
coalition.ato.add_package(self.package)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -99,7 +97,6 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
fulfiller = PackageFulfiller(
|
fulfiller = PackageFulfiller(
|
||||||
state.context.coalition,
|
state.context.coalition,
|
||||||
state.context.theater,
|
state.context.theater,
|
||||||
state.available_aircraft,
|
|
||||||
state.context.settings,
|
state.context.settings,
|
||||||
)
|
)
|
||||||
self.package = fulfiller.plan_mission(
|
self.package = fulfiller.plan_mission(
|
||||||
|
|||||||
@ -10,9 +10,9 @@ from typing import TYPE_CHECKING, Any, Union, Optional
|
|||||||
from game.commander.garrisons import Garrisons
|
from game.commander.garrisons import Garrisons
|
||||||
from game.commander.objectivefinder import ObjectiveFinder
|
from game.commander.objectivefinder import ObjectiveFinder
|
||||||
from game.htn import WorldState
|
from game.htn import WorldState
|
||||||
from game.inventory import GlobalAircraftInventory
|
|
||||||
from game.profiling import MultiEventTracer
|
from game.profiling import MultiEventTracer
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
|
from game.squadrons import AirWing
|
||||||
from game.theater import ControlPoint, FrontLine, MissionTarget, ConflictTheater
|
from game.theater import ControlPoint, FrontLine, MissionTarget, ConflictTheater
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
@ -58,7 +58,6 @@ class TheaterState(WorldState["TheaterState"]):
|
|||||||
strike_targets: list[TheaterGroundObject[Any]]
|
strike_targets: list[TheaterGroundObject[Any]]
|
||||||
enemy_barcaps: list[ControlPoint]
|
enemy_barcaps: list[ControlPoint]
|
||||||
threat_zones: ThreatZones
|
threat_zones: ThreatZones
|
||||||
available_aircraft: GlobalAircraftInventory
|
|
||||||
|
|
||||||
def _rebuild_threat_zones(self) -> None:
|
def _rebuild_threat_zones(self) -> None:
|
||||||
"""Recreates the theater's threat zones based on the current planned state."""
|
"""Recreates the theater's threat zones based on the current planned state."""
|
||||||
@ -122,7 +121,6 @@ class TheaterState(WorldState["TheaterState"]):
|
|||||||
strike_targets=list(self.strike_targets),
|
strike_targets=list(self.strike_targets),
|
||||||
enemy_barcaps=list(self.enemy_barcaps),
|
enemy_barcaps=list(self.enemy_barcaps),
|
||||||
threat_zones=self.threat_zones,
|
threat_zones=self.threat_zones,
|
||||||
available_aircraft=self.available_aircraft.clone(),
|
|
||||||
# Persistent properties are not copied. These are a way for failed subtasks
|
# Persistent properties are not copied. These are a way for failed subtasks
|
||||||
# to communicate requirements to other tasks. For example, the task to
|
# to communicate requirements to other tasks. For example, the task to
|
||||||
# attack enemy garrisons might fail because the target area has IADS
|
# attack enemy garrisons might fail because the target area has IADS
|
||||||
@ -172,5 +170,4 @@ class TheaterState(WorldState["TheaterState"]):
|
|||||||
strike_targets=list(finder.strike_targets()),
|
strike_targets=list(finder.strike_targets()),
|
||||||
enemy_barcaps=list(game.theater.control_points_for(not player)),
|
enemy_barcaps=list(game.theater.control_points_for(not player)),
|
||||||
threat_zones=game.threat_zone_for(not player),
|
threat_zones=game.threat_zone_for(not player),
|
||||||
available_aircraft=game.aircraft_inventory.clone(),
|
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,13 +7,12 @@ from dcs.mapping import Point
|
|||||||
from dcs.task import Task
|
from dcs.task import Task
|
||||||
|
|
||||||
from game import persistency
|
from game import persistency
|
||||||
from game.debriefing import AirLosses, Debriefing
|
from game.debriefing import Debriefing
|
||||||
from game.infos.information import Information
|
from game.infos.information import Information
|
||||||
from game.operation.operation import Operation
|
from game.operation.operation import Operation
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
from gen.ato import AirTaskingOrder
|
from gen.ato import AirTaskingOrder
|
||||||
from gen.ground_forces.combat_stance import CombatStance
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
from ..dcs.groundunittype import GroundUnitType
|
|
||||||
from ..unitmap import UnitMap
|
from ..unitmap import UnitMap
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -67,59 +66,6 @@ class Event:
|
|||||||
)
|
)
|
||||||
return unit_map
|
return unit_map
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _transfer_aircraft(
|
|
||||||
ato: AirTaskingOrder, losses: AirLosses, for_player: bool
|
|
||||||
) -> None:
|
|
||||||
for package in ato.packages:
|
|
||||||
for flight in package.flights:
|
|
||||||
# No need to transfer to the same location.
|
|
||||||
if flight.departure == flight.arrival:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Don't transfer to bases that were captured. Note that if the
|
|
||||||
# airfield was back-filling transfers it may overflow. We could
|
|
||||||
# attempt to be smarter in the future by performing transfers in
|
|
||||||
# order up a graph to prevent transfers to full airports and
|
|
||||||
# send overflow off-map, but overflow is fine for now.
|
|
||||||
if flight.arrival.captured != for_player:
|
|
||||||
logging.info(
|
|
||||||
f"Not transferring {flight} because {flight.arrival} "
|
|
||||||
"was captured"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
transfer_count = losses.surviving_flight_members(flight)
|
|
||||||
if transfer_count < 0:
|
|
||||||
logging.error(
|
|
||||||
f"{flight} had {flight.count} aircraft but "
|
|
||||||
f"{transfer_count} losses were recorded."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
aircraft = flight.unit_type
|
|
||||||
available = flight.departure.base.total_units_of_type(aircraft)
|
|
||||||
if available < transfer_count:
|
|
||||||
logging.error(
|
|
||||||
f"Found killed {aircraft} from {flight.departure} but "
|
|
||||||
f"that airbase has only {available} available."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
flight.departure.base.aircraft[aircraft] -= transfer_count
|
|
||||||
if aircraft not in flight.arrival.base.aircraft:
|
|
||||||
# TODO: Should use defaultdict.
|
|
||||||
flight.arrival.base.aircraft[aircraft] = 0
|
|
||||||
flight.arrival.base.aircraft[aircraft] += transfer_count
|
|
||||||
|
|
||||||
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
|
|
||||||
self._transfer_aircraft(
|
|
||||||
self.game.blue.ato, debriefing.air_losses, for_player=True
|
|
||||||
)
|
|
||||||
self._transfer_aircraft(
|
|
||||||
self.game.red.ato, debriefing.air_losses, for_player=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
||||||
for loss in debriefing.air_losses.losses:
|
for loss in debriefing.air_losses.losses:
|
||||||
if loss.pilot is not None and (
|
if loss.pilot is not None and (
|
||||||
@ -127,18 +73,18 @@ class Event:
|
|||||||
or not self.game.settings.invulnerable_player_pilots
|
or not self.game.settings.invulnerable_player_pilots
|
||||||
):
|
):
|
||||||
loss.pilot.kill()
|
loss.pilot.kill()
|
||||||
|
squadron = loss.flight.squadron
|
||||||
aircraft = loss.flight.unit_type
|
aircraft = loss.flight.unit_type
|
||||||
cp = loss.flight.departure
|
available = squadron.owned_aircraft
|
||||||
available = cp.base.total_units_of_type(aircraft)
|
|
||||||
if available <= 0:
|
if available <= 0:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Found killed {aircraft} from {cp} but that airbase has "
|
f"Found killed {aircraft} from {squadron} but that airbase has "
|
||||||
"none available."
|
"none available."
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logging.info(f"{aircraft} destroyed from {cp}")
|
logging.info(f"{aircraft} destroyed from {squadron}")
|
||||||
cp.base.aircraft[aircraft] -= 1
|
squadron.owned_aircraft -= 1
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
|
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
|
||||||
@ -276,7 +222,6 @@ class Event:
|
|||||||
self.commit_building_losses(debriefing)
|
self.commit_building_losses(debriefing)
|
||||||
self.commit_damaged_runways(debriefing)
|
self.commit_damaged_runways(debriefing)
|
||||||
self.commit_captures(debriefing)
|
self.commit_captures(debriefing)
|
||||||
self.complete_aircraft_transfers(debriefing)
|
|
||||||
|
|
||||||
# Destroyed units carcass
|
# Destroyed units carcass
|
||||||
# -------------------------
|
# -------------------------
|
||||||
@ -458,15 +403,10 @@ class Event:
|
|||||||
source.base.commit_losses(moved_units)
|
source.base.commit_losses(moved_units)
|
||||||
|
|
||||||
# Also transfer pending deliveries.
|
# Also transfer pending deliveries.
|
||||||
for unit_type, count in source.pending_unit_deliveries.units.items():
|
for unit_type, count in source.ground_unit_orders.units.items():
|
||||||
if not isinstance(unit_type, GroundUnitType):
|
|
||||||
continue
|
|
||||||
if count <= 0:
|
|
||||||
# Don't transfer *sales*...
|
|
||||||
continue
|
|
||||||
move_count = int(count * move_factor)
|
move_count = int(count * move_factor)
|
||||||
source.pending_unit_deliveries.sell({unit_type: move_count})
|
source.ground_unit_orders.sell({unit_type: move_count})
|
||||||
destination.pending_unit_deliveries.order({unit_type: move_count})
|
destination.ground_unit_orders.order({unit_type: move_count})
|
||||||
total_units_redeployed += move_count
|
total_units_redeployed += move_count
|
||||||
|
|
||||||
if total_units_redeployed > 0:
|
if total_units_redeployed > 0:
|
||||||
|
|||||||
13
game/game.py
13
game/game.py
@ -13,7 +13,6 @@ from dcs.task import CAP, CAS, PinpointStrike
|
|||||||
from dcs.vehicles import AirDefence
|
from dcs.vehicles import AirDefence
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
|
|
||||||
from game.inventory import GlobalAircraftInventory
|
|
||||||
from game.models.game_stats import GameStats
|
from game.models.game_stats import GameStats
|
||||||
from game.plugins import LuaPluginManager
|
from game.plugins import LuaPluginManager
|
||||||
from gen import naming
|
from gen import naming
|
||||||
@ -127,8 +126,6 @@ class Game:
|
|||||||
self.blue.configure_default_air_wing(air_wing_config)
|
self.blue.configure_default_air_wing(air_wing_config)
|
||||||
self.red.configure_default_air_wing(air_wing_config)
|
self.red.configure_default_air_wing(air_wing_config)
|
||||||
|
|
||||||
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
|
|
||||||
|
|
||||||
self.on_load(game_still_initializing=True)
|
self.on_load(game_still_initializing=True)
|
||||||
|
|
||||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
@ -392,9 +389,9 @@ class Game:
|
|||||||
|
|
||||||
# Plan Coalition specific turn
|
# Plan Coalition specific turn
|
||||||
if for_blue:
|
if for_blue:
|
||||||
self.initialize_turn_for(player=True)
|
self.blue.initialize_turn()
|
||||||
if for_red:
|
if for_red:
|
||||||
self.initialize_turn_for(player=False)
|
self.red.initialize_turn()
|
||||||
|
|
||||||
# Plan GroundWar
|
# Plan GroundWar
|
||||||
self.ground_planners = {}
|
self.ground_planners = {}
|
||||||
@ -404,12 +401,6 @@ class Game:
|
|||||||
gplanner.plan_groundwar()
|
gplanner.plan_groundwar()
|
||||||
self.ground_planners[cp.id] = gplanner
|
self.ground_planners[cp.id] = gplanner
|
||||||
|
|
||||||
def initialize_turn_for(self, player: bool) -> None:
|
|
||||||
self.aircraft_inventory.reset(player)
|
|
||||||
for cp in self.theater.control_points_for(player):
|
|
||||||
self.aircraft_inventory.set_from_control_point(cp)
|
|
||||||
self.coalition_for(player).initialize_turn()
|
|
||||||
|
|
||||||
def message(self, text: str) -> None:
|
def message(self, text: str) -> None:
|
||||||
self.informations.append(Information(text, turn=self.turn))
|
self.informations.append(Information(text, turn=self.turn))
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from typing import Optional, TYPE_CHECKING
|
||||||
from typing import Optional, TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
from .coalition import Coalition
|
from .coalition import Coalition
|
||||||
from .dcs.groundunittype import GroundUnitType
|
from .dcs.groundunittype import GroundUnitType
|
||||||
from .dcs.unittype import UnitType
|
|
||||||
from .theater.transitnetwork import (
|
from .theater.transitnetwork import (
|
||||||
NoPathError,
|
NoPathError,
|
||||||
TransitNetwork,
|
TransitNetwork,
|
||||||
@ -19,58 +17,41 @@ if TYPE_CHECKING:
|
|||||||
from .game import Game
|
from .game import Game
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
class GroundUnitOrders:
|
||||||
class GroundUnitSource:
|
|
||||||
control_point: ControlPoint
|
|
||||||
|
|
||||||
|
|
||||||
class PendingUnitDeliveries:
|
|
||||||
def __init__(self, destination: ControlPoint) -> None:
|
def __init__(self, destination: ControlPoint) -> None:
|
||||||
self.destination = destination
|
self.destination = destination
|
||||||
|
|
||||||
# Maps unit type to order quantity.
|
# Maps unit type to order quantity.
|
||||||
self.units: dict[UnitType[Any], int] = defaultdict(int)
|
self.units: dict[GroundUnitType, int] = defaultdict(int)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Pending delivery to {self.destination}"
|
return f"Pending ground unit delivery to {self.destination}"
|
||||||
|
|
||||||
def order(self, units: dict[UnitType[Any], int]) -> None:
|
def order(self, units: dict[GroundUnitType, int]) -> None:
|
||||||
for k, v in units.items():
|
for k, v in units.items():
|
||||||
self.units[k] += v
|
self.units[k] += v
|
||||||
|
|
||||||
def sell(self, units: dict[UnitType[Any], int]) -> None:
|
def sell(self, units: dict[GroundUnitType, int]) -> None:
|
||||||
for k, v in units.items():
|
for k, v in units.items():
|
||||||
self.units[k] -= v
|
self.units[k] -= v
|
||||||
if self.units[k] == 0:
|
if self.units[k] == 0:
|
||||||
del self.units[k]
|
del self.units[k]
|
||||||
|
|
||||||
def refund_all(self, coalition: Coalition) -> None:
|
def refund_all(self, coalition: Coalition) -> None:
|
||||||
self.refund(coalition, self.units)
|
self._refund(coalition, self.units)
|
||||||
self.units = defaultdict(int)
|
self.units = defaultdict(int)
|
||||||
|
|
||||||
def refund_ground_units(self, coalition: Coalition) -> None:
|
def _refund(self, coalition: Coalition, units: dict[GroundUnitType, int]) -> None:
|
||||||
ground_units: dict[UnitType[Any], int] = {
|
|
||||||
u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType)
|
|
||||||
}
|
|
||||||
self.refund(coalition, ground_units)
|
|
||||||
for gu in ground_units.keys():
|
|
||||||
del self.units[gu]
|
|
||||||
|
|
||||||
def refund(self, coalition: Coalition, units: dict[UnitType[Any], int]) -> None:
|
|
||||||
for unit_type, count in units.items():
|
for unit_type, count in units.items():
|
||||||
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
|
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
|
||||||
coalition.adjust_budget(unit_type.price * count)
|
coalition.adjust_budget(unit_type.price * count)
|
||||||
|
|
||||||
def pending_orders(self, unit_type: UnitType[Any]) -> int:
|
def pending_orders(self, unit_type: GroundUnitType) -> int:
|
||||||
pending_units = self.units.get(unit_type)
|
pending_units = self.units.get(unit_type)
|
||||||
if pending_units is None:
|
if pending_units is None:
|
||||||
pending_units = 0
|
pending_units = 0
|
||||||
return pending_units
|
return pending_units
|
||||||
|
|
||||||
def available_next_turn(self, unit_type: UnitType[Any]) -> int:
|
|
||||||
current_units = self.destination.base.total_units_of_type(unit_type)
|
|
||||||
return self.pending_orders(unit_type) + current_units
|
|
||||||
|
|
||||||
def process(self, game: Game) -> None:
|
def process(self, game: Game) -> None:
|
||||||
coalition = game.coalition_for(self.destination.captured)
|
coalition = game.coalition_for(self.destination.captured)
|
||||||
ground_unit_source = self.find_ground_unit_source(game)
|
ground_unit_source = self.find_ground_unit_source(game)
|
||||||
@ -79,36 +60,33 @@ class PendingUnitDeliveries:
|
|||||||
f"{self.destination.name} lost its source for ground unit "
|
f"{self.destination.name} lost its source for ground unit "
|
||||||
"reinforcements. Refunding purchase price."
|
"reinforcements. Refunding purchase price."
|
||||||
)
|
)
|
||||||
self.refund_ground_units(coalition)
|
self.refund_all(coalition)
|
||||||
|
|
||||||
bought_units: dict[UnitType[Any], int] = {}
|
bought_units: dict[GroundUnitType, int] = {}
|
||||||
units_needing_transfer: dict[GroundUnitType, int] = {}
|
units_needing_transfer: dict[GroundUnitType, int] = {}
|
||||||
sold_units: dict[UnitType[Any], int] = {}
|
|
||||||
for unit_type, count in self.units.items():
|
for unit_type, count in self.units.items():
|
||||||
allegiance = "Ally" if self.destination.captured else "Enemy"
|
allegiance = "Ally" if self.destination.captured else "Enemy"
|
||||||
d: dict[Any, int]
|
d: dict[GroundUnitType, int]
|
||||||
if (
|
if self.destination != ground_unit_source:
|
||||||
isinstance(unit_type, GroundUnitType)
|
|
||||||
and self.destination != ground_unit_source
|
|
||||||
):
|
|
||||||
source = ground_unit_source
|
source = ground_unit_source
|
||||||
d = units_needing_transfer
|
d = units_needing_transfer
|
||||||
else:
|
else:
|
||||||
source = self.destination
|
source = self.destination
|
||||||
d = bought_units
|
d = bought_units
|
||||||
|
|
||||||
if count >= 0:
|
if count < 0:
|
||||||
|
logging.error(
|
||||||
|
f"Attempted sale of {unit_type} at {self.destination} but ground "
|
||||||
|
"units cannot be sold"
|
||||||
|
)
|
||||||
|
elif count > 0:
|
||||||
d[unit_type] = count
|
d[unit_type] = count
|
||||||
game.message(
|
game.message(
|
||||||
f"{allegiance} reinforcements: {unit_type} x {count} at {source}"
|
f"{allegiance} reinforcements: {unit_type} x {count} at {source}"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
sold_units[unit_type] = -count
|
|
||||||
game.message(f"{allegiance} sold: {unit_type} x {-count} at {source}")
|
|
||||||
|
|
||||||
self.units = defaultdict(int)
|
self.units = defaultdict(int)
|
||||||
self.destination.base.commission_units(bought_units)
|
self.destination.base.commission_units(bought_units)
|
||||||
self.destination.base.commit_losses(sold_units)
|
|
||||||
|
|
||||||
if units_needing_transfer:
|
if units_needing_transfer:
|
||||||
if ground_unit_source is None:
|
if ground_unit_source is None:
|
||||||
@ -1,142 +0,0 @@
|
|||||||
"""Inventory management APIs."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections import defaultdict, Iterator, Iterable
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game.theater import ControlPoint
|
|
||||||
from gen.flights.flight import Flight
|
|
||||||
|
|
||||||
|
|
||||||
class ControlPointAircraftInventory:
|
|
||||||
"""Aircraft inventory for a single control point."""
|
|
||||||
|
|
||||||
def __init__(self, control_point: ControlPoint) -> None:
|
|
||||||
self.control_point = control_point
|
|
||||||
self.inventory: dict[AircraftType, int] = defaultdict(int)
|
|
||||||
|
|
||||||
def clone(self) -> ControlPointAircraftInventory:
|
|
||||||
new = ControlPointAircraftInventory(self.control_point)
|
|
||||||
new.inventory = self.inventory.copy()
|
|
||||||
return new
|
|
||||||
|
|
||||||
def add_aircraft(self, aircraft: AircraftType, count: int) -> None:
|
|
||||||
"""Adds aircraft to the inventory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
aircraft: The type of aircraft to add.
|
|
||||||
count: The number of aircraft to add.
|
|
||||||
"""
|
|
||||||
self.inventory[aircraft] += count
|
|
||||||
|
|
||||||
def remove_aircraft(self, aircraft: AircraftType, count: int) -> None:
|
|
||||||
"""Removes aircraft from the inventory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
aircraft: The type of aircraft to remove.
|
|
||||||
count: The number of aircraft to remove.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: The control point cannot fulfill the requested number of
|
|
||||||
aircraft.
|
|
||||||
"""
|
|
||||||
available = self.inventory[aircraft]
|
|
||||||
if available < count:
|
|
||||||
raise ValueError(
|
|
||||||
f"Cannot remove {count} {aircraft} from "
|
|
||||||
f"{self.control_point.name}. Only have {available}."
|
|
||||||
)
|
|
||||||
self.inventory[aircraft] -= count
|
|
||||||
|
|
||||||
def available(self, aircraft: AircraftType) -> int:
|
|
||||||
"""Returns the number of available aircraft of the given type.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
aircraft: The type of aircraft to query.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self.inventory[aircraft]
|
|
||||||
except KeyError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def types_available(self) -> Iterator[AircraftType]:
|
|
||||||
"""Iterates over all available aircraft types."""
|
|
||||||
for aircraft, count in self.inventory.items():
|
|
||||||
if count > 0:
|
|
||||||
yield aircraft
|
|
||||||
|
|
||||||
@property
|
|
||||||
def all_aircraft(self) -> Iterator[tuple[AircraftType, int]]:
|
|
||||||
"""Iterates over all available aircraft types, including amounts."""
|
|
||||||
for aircraft, count in self.inventory.items():
|
|
||||||
if count > 0:
|
|
||||||
yield aircraft, count
|
|
||||||
|
|
||||||
def clear(self) -> None:
|
|
||||||
"""Clears all aircraft from the inventory."""
|
|
||||||
self.inventory.clear()
|
|
||||||
|
|
||||||
|
|
||||||
class GlobalAircraftInventory:
|
|
||||||
"""Game-wide aircraft inventory."""
|
|
||||||
|
|
||||||
def __init__(self, control_points: Iterable[ControlPoint]) -> None:
|
|
||||||
self.inventories: dict[ControlPoint, ControlPointAircraftInventory] = {
|
|
||||||
cp: ControlPointAircraftInventory(cp) for cp in control_points
|
|
||||||
}
|
|
||||||
|
|
||||||
def clone(self) -> GlobalAircraftInventory:
|
|
||||||
new = GlobalAircraftInventory([])
|
|
||||||
new.inventories = {
|
|
||||||
cp: inventory.clone() for cp, inventory in self.inventories.items()
|
|
||||||
}
|
|
||||||
return new
|
|
||||||
|
|
||||||
def reset(self, for_player: bool) -> None:
|
|
||||||
"""Clears the inventory of every control point owned by the given coalition."""
|
|
||||||
for inventory in self.inventories.values():
|
|
||||||
if inventory.control_point.captured == for_player:
|
|
||||||
inventory.clear()
|
|
||||||
|
|
||||||
def set_from_control_point(self, control_point: ControlPoint) -> None:
|
|
||||||
"""Set the control point's aircraft inventory.
|
|
||||||
|
|
||||||
If the inventory for the given control point has already been set for
|
|
||||||
the turn, it will be overwritten.
|
|
||||||
"""
|
|
||||||
inventory = self.inventories[control_point]
|
|
||||||
for aircraft, count in control_point.base.aircraft.items():
|
|
||||||
inventory.add_aircraft(aircraft, count)
|
|
||||||
|
|
||||||
def for_control_point(
|
|
||||||
self, control_point: ControlPoint
|
|
||||||
) -> ControlPointAircraftInventory:
|
|
||||||
"""Returns the inventory specific to the given control point."""
|
|
||||||
return self.inventories[control_point]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available_types_for_player(self) -> Iterator[AircraftType]:
|
|
||||||
"""Iterates over all aircraft types available to the player."""
|
|
||||||
seen: set[AircraftType] = set()
|
|
||||||
for control_point, inventory in self.inventories.items():
|
|
||||||
if control_point.captured:
|
|
||||||
for aircraft in inventory.types_available:
|
|
||||||
if not control_point.can_operate(aircraft):
|
|
||||||
continue
|
|
||||||
if aircraft not in seen:
|
|
||||||
seen.add(aircraft)
|
|
||||||
yield aircraft
|
|
||||||
|
|
||||||
def claim_for_flight(self, flight: Flight) -> None:
|
|
||||||
"""Removes aircraft from the inventory for the given flight."""
|
|
||||||
inventory = self.for_control_point(flight.from_cp)
|
|
||||||
inventory.remove_aircraft(flight.unit_type, flight.count)
|
|
||||||
|
|
||||||
def return_from_flight(self, flight: Flight) -> None:
|
|
||||||
"""Returns a flight's aircraft to the inventory."""
|
|
||||||
inventory = self.for_control_point(flight.from_cp)
|
|
||||||
inventory.add_aircraft(flight.unit_type, flight.count)
|
|
||||||
@ -56,10 +56,12 @@ class GameStats:
|
|||||||
|
|
||||||
for cp in game.theater.controlpoints:
|
for cp in game.theater.controlpoints:
|
||||||
if cp.captured:
|
if cp.captured:
|
||||||
turn_data.allied_units.aircraft_count += sum(cp.base.aircraft.values())
|
for squadron in cp.squadrons:
|
||||||
|
turn_data.allied_units.aircraft_count += squadron.owned_aircraft
|
||||||
turn_data.allied_units.vehicles_count += sum(cp.base.armor.values())
|
turn_data.allied_units.vehicles_count += sum(cp.base.armor.values())
|
||||||
else:
|
else:
|
||||||
turn_data.enemy_units.aircraft_count += sum(cp.base.aircraft.values())
|
for squadron in cp.squadrons:
|
||||||
|
turn_data.enemy_units.aircraft_count += squadron.owned_aircraft
|
||||||
turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values())
|
turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values())
|
||||||
|
|
||||||
self.data_per_turn.append(turn_data)
|
self.data_per_turn.append(turn_data)
|
||||||
|
|||||||
@ -7,7 +7,6 @@ from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
|||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.groundunitclass import GroundUnitClass
|
||||||
from game.dcs.aircrafttype import AircraftType
|
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from game.squadrons import Squadron
|
from game.squadrons import Squadron
|
||||||
@ -98,37 +97,10 @@ class ProcurementAi:
|
|||||||
budget -= armor_budget
|
budget -= armor_budget
|
||||||
budget += self.reinforce_front_line(armor_budget)
|
budget += self.reinforce_front_line(armor_budget)
|
||||||
|
|
||||||
# Don't sell overstock aircraft until after we've bought runways and
|
|
||||||
# front lines. Any budget we free up should be earmarked for aircraft.
|
|
||||||
if not self.is_player:
|
|
||||||
budget += self.sell_incomplete_squadrons()
|
|
||||||
if self.manage_aircraft:
|
if self.manage_aircraft:
|
||||||
budget = self.purchase_aircraft(budget)
|
budget = self.purchase_aircraft(budget)
|
||||||
return budget
|
return budget
|
||||||
|
|
||||||
def sell_incomplete_squadrons(self) -> float:
|
|
||||||
# Selling incomplete squadrons gives us more money to spend on the next
|
|
||||||
# turn. This serves as a short term fix for
|
|
||||||
# https://github.com/dcs-liberation/dcs_liberation/issues/41.
|
|
||||||
#
|
|
||||||
# Only incomplete squadrons which are unlikely to get used will be sold
|
|
||||||
# rather than all unused aircraft because the unused aircraft are what
|
|
||||||
# make OCA strikes worthwhile.
|
|
||||||
#
|
|
||||||
# This option is only used by the AI since players cannot cancel sales
|
|
||||||
# (https://github.com/dcs-liberation/dcs_liberation/issues/365).
|
|
||||||
total = 0.0
|
|
||||||
for cp in self.game.theater.control_points_for(self.is_player):
|
|
||||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
|
||||||
for aircraft, available in inventory.all_aircraft:
|
|
||||||
# We only ever plan even groups, so the odd aircraft is unlikely
|
|
||||||
# to get used.
|
|
||||||
if available % 2 == 0:
|
|
||||||
continue
|
|
||||||
inventory.remove_aircraft(aircraft, 1)
|
|
||||||
total += aircraft.price
|
|
||||||
return total
|
|
||||||
|
|
||||||
def repair_runways(self, budget: float) -> float:
|
def repair_runways(self, budget: float) -> float:
|
||||||
for control_point in self.owned_points:
|
for control_point in self.owned_points:
|
||||||
if budget < db.RUNWAY_REPAIR_COST:
|
if budget < db.RUNWAY_REPAIR_COST:
|
||||||
@ -181,7 +153,7 @@ class ProcurementAi:
|
|||||||
break
|
break
|
||||||
|
|
||||||
budget -= unit.price
|
budget -= unit.price
|
||||||
cp.pending_unit_deliveries.order({unit: 1})
|
cp.ground_unit_orders.order({unit: 1})
|
||||||
|
|
||||||
return budget
|
return budget
|
||||||
|
|
||||||
@ -211,64 +183,28 @@ class ProcurementAi:
|
|||||||
return worst_balanced
|
return worst_balanced
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _compatible_squadron_at(
|
|
||||||
aircraft: AircraftType, airbase: ControlPoint, task: FlightType, count: int
|
|
||||||
) -> Optional[Squadron]:
|
|
||||||
for squadron in airbase.squadrons:
|
|
||||||
if squadron.aircraft != aircraft:
|
|
||||||
continue
|
|
||||||
if not squadron.can_auto_assign(task):
|
|
||||||
continue
|
|
||||||
if not squadron.can_provide_pilots(count):
|
|
||||||
continue
|
|
||||||
return squadron
|
|
||||||
return None
|
|
||||||
|
|
||||||
def affordable_aircraft_for(
|
|
||||||
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
|
|
||||||
) -> Optional[AircraftType]:
|
|
||||||
for unit in aircraft_for_task(request.task_capability):
|
|
||||||
if unit.price * request.number > budget:
|
|
||||||
continue
|
|
||||||
|
|
||||||
squadron = self._compatible_squadron_at(
|
|
||||||
unit, airbase, request.task_capability, request.number
|
|
||||||
)
|
|
||||||
if squadron is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
distance_to_target = meters(request.near.distance_to(airbase))
|
|
||||||
if distance_to_target > unit.max_mission_range:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Affordable, compatible, and we have a squadron capable of the task.
|
|
||||||
return unit
|
|
||||||
return None
|
|
||||||
|
|
||||||
def fulfill_aircraft_request(
|
def fulfill_aircraft_request(
|
||||||
self, request: AircraftProcurementRequest, budget: float
|
squadrons: list[Squadron], quantity: int, budget: float
|
||||||
) -> Tuple[float, bool]:
|
) -> Tuple[float, bool]:
|
||||||
for airbase in self.best_airbases_for(request):
|
for squadron in squadrons:
|
||||||
unit = self.affordable_aircraft_for(request, airbase, budget)
|
price = squadron.aircraft.price * quantity
|
||||||
if unit is None:
|
if price > budget:
|
||||||
# Can't afford any aircraft capable of performing the
|
|
||||||
# required mission that can operate from this airbase. We
|
|
||||||
# might be able to afford aircraft at other airbases though,
|
|
||||||
# in the case where the airbase we attempted to use is only
|
|
||||||
# able to operate expensive aircraft.
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
budget -= unit.price * request.number
|
squadron.pending_deliveries += quantity
|
||||||
airbase.pending_unit_deliveries.order({unit: request.number})
|
budget -= price
|
||||||
return budget, True
|
return budget, True
|
||||||
return budget, False
|
return budget, False
|
||||||
|
|
||||||
def purchase_aircraft(self, budget: float) -> float:
|
def purchase_aircraft(self, budget: float) -> float:
|
||||||
for request in self.game.coalition_for(self.is_player).procurement_requests:
|
for request in self.game.coalition_for(self.is_player).procurement_requests:
|
||||||
if not list(self.best_airbases_for(request)):
|
squadrons = list(self.best_squadrons_for(request))
|
||||||
|
if not squadrons:
|
||||||
# No airbases in range of this request. Skip it.
|
# No airbases in range of this request. Skip it.
|
||||||
continue
|
continue
|
||||||
budget, fulfilled = self.fulfill_aircraft_request(request, budget)
|
budget, fulfilled = self.fulfill_aircraft_request(
|
||||||
|
squadrons, request.number, budget
|
||||||
|
)
|
||||||
if not fulfilled:
|
if not fulfilled:
|
||||||
# The request was not fulfilled because we could not afford any suitable
|
# The request was not fulfilled because we could not afford any suitable
|
||||||
# aircraft. Rather than continuing, which could proceed to buy tons of
|
# aircraft. Rather than continuing, which could proceed to buy tons of
|
||||||
@ -285,9 +221,32 @@ class ProcurementAi:
|
|||||||
else:
|
else:
|
||||||
return self.game.theater.enemy_points()
|
return self.game.theater.enemy_points()
|
||||||
|
|
||||||
def best_airbases_for(
|
@staticmethod
|
||||||
|
def squadron_rank_for_task(squadron: Squadron, task: FlightType) -> int:
|
||||||
|
return aircraft_for_task(task).index(squadron.aircraft)
|
||||||
|
|
||||||
|
def compatible_squadrons_at_airbase(
|
||||||
|
self, airbase: ControlPoint, request: AircraftProcurementRequest
|
||||||
|
) -> Iterator[Squadron]:
|
||||||
|
compatible: list[Squadron] = []
|
||||||
|
for squadron in airbase.squadrons:
|
||||||
|
if not squadron.can_auto_assign(request.task_capability):
|
||||||
|
continue
|
||||||
|
if not squadron.can_provide_pilots(request.number):
|
||||||
|
continue
|
||||||
|
|
||||||
|
distance_to_target = meters(request.near.distance_to(airbase))
|
||||||
|
if distance_to_target > squadron.aircraft.max_mission_range:
|
||||||
|
continue
|
||||||
|
compatible.append(squadron)
|
||||||
|
yield from sorted(
|
||||||
|
compatible,
|
||||||
|
key=lambda s: self.squadron_rank_for_task(s, request.task_capability),
|
||||||
|
)
|
||||||
|
|
||||||
|
def best_squadrons_for(
|
||||||
self, request: AircraftProcurementRequest
|
self, request: AircraftProcurementRequest
|
||||||
) -> Iterator[ControlPoint]:
|
) -> Iterator[Squadron]:
|
||||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
|
distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
|
||||||
threatened = []
|
threatened = []
|
||||||
for cp in distance_cache.operational_airfields:
|
for cp in distance_cache.operational_airfields:
|
||||||
@ -297,8 +256,10 @@ class ProcurementAi:
|
|||||||
continue
|
continue
|
||||||
if self.threat_zones.threatened(cp.position):
|
if self.threat_zones.threatened(cp.position):
|
||||||
threatened.append(cp)
|
threatened.append(cp)
|
||||||
yield cp
|
continue
|
||||||
yield from threatened
|
yield from self.compatible_squadrons_at_airbase(cp, request)
|
||||||
|
for threatened_base in threatened:
|
||||||
|
yield from self.compatible_squadrons_at_airbase(threatened_base, request)
|
||||||
|
|
||||||
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
|
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
|
||||||
worst_supply = math.inf
|
worst_supply = math.inf
|
||||||
|
|||||||
180
game/purchaseadapter.py
Normal file
180
game/purchaseadapter.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
from abc import abstractmethod
|
||||||
|
from typing import TypeVar, Generic
|
||||||
|
|
||||||
|
from game import Game
|
||||||
|
from game.coalition import Coalition
|
||||||
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
|
from game.squadrons import Squadron
|
||||||
|
from game.theater import ControlPoint
|
||||||
|
|
||||||
|
ItemType = TypeVar("ItemType")
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionError(RuntimeError):
|
||||||
|
def __init__(self, message: str) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseAdapter(Generic[ItemType]):
|
||||||
|
def __init__(self, coalition: Coalition) -> None:
|
||||||
|
self.coalition = coalition
|
||||||
|
|
||||||
|
def buy(self, item: ItemType, quantity: int) -> None:
|
||||||
|
for _ in range(quantity):
|
||||||
|
if self.has_pending_sales(item):
|
||||||
|
self.do_cancel_sale(item)
|
||||||
|
elif self.can_buy(item):
|
||||||
|
self.do_purchase(item)
|
||||||
|
else:
|
||||||
|
raise TransactionError(f"Cannot buy more {item}")
|
||||||
|
self.coalition.adjust_budget(-self.price_of(item))
|
||||||
|
|
||||||
|
def sell(self, item: ItemType, quantity: int) -> None:
|
||||||
|
for _ in range(quantity):
|
||||||
|
if self.has_pending_orders(item):
|
||||||
|
self.do_cancel_purchase(item)
|
||||||
|
elif self.can_sell(item):
|
||||||
|
self.do_sale(item)
|
||||||
|
else:
|
||||||
|
raise TransactionError(f"Cannot sell more {item}")
|
||||||
|
self.coalition.adjust_budget(self.price_of(item))
|
||||||
|
|
||||||
|
def has_pending_orders(self, item: ItemType) -> bool:
|
||||||
|
return self.pending_delivery_quantity(item) > 0
|
||||||
|
|
||||||
|
def has_pending_sales(self, item: ItemType) -> bool:
|
||||||
|
return self.pending_delivery_quantity(item) < 0
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def current_quantity_of(self, item: ItemType) -> int:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def pending_delivery_quantity(self, item: ItemType) -> int:
|
||||||
|
...
|
||||||
|
|
||||||
|
def expected_quantity_next_turn(self, item: ItemType) -> int:
|
||||||
|
return self.current_quantity_of(item) + self.pending_delivery_quantity(item)
|
||||||
|
|
||||||
|
def can_buy(self, item: ItemType) -> bool:
|
||||||
|
return self.coalition.budget >= self.price_of(item)
|
||||||
|
|
||||||
|
def can_sell_or_cancel(self, item: ItemType) -> bool:
|
||||||
|
return self.can_sell(item) or self.has_pending_orders(item)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def can_sell(self, item: ItemType) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def do_purchase(self, item: ItemType) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def do_cancel_purchase(self, item: ItemType) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def do_sale(self, item: ItemType) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def do_cancel_sale(self, item: ItemType) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def price_of(self, item: ItemType) -> int:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def name_of(self, item: ItemType, multiline: bool = False) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
|
||||||
|
def __init__(
|
||||||
|
self, control_point: ControlPoint, coalition: Coalition, game: Game
|
||||||
|
) -> None:
|
||||||
|
super().__init__(coalition)
|
||||||
|
self.control_point = control_point
|
||||||
|
self.game = game
|
||||||
|
|
||||||
|
def pending_delivery_quantity(self, item: Squadron) -> int:
|
||||||
|
return item.pending_deliveries
|
||||||
|
|
||||||
|
def current_quantity_of(self, item: Squadron) -> int:
|
||||||
|
return item.owned_aircraft
|
||||||
|
|
||||||
|
def can_buy(self, item: Squadron) -> bool:
|
||||||
|
return (
|
||||||
|
super().can_buy(item)
|
||||||
|
and self.control_point.unclaimed_parking(self.game) > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
def can_sell(self, item: Squadron) -> bool:
|
||||||
|
return item.untasked_aircraft > 0
|
||||||
|
|
||||||
|
def do_purchase(self, item: Squadron) -> None:
|
||||||
|
item.pending_deliveries += 1
|
||||||
|
|
||||||
|
def do_cancel_purchase(self, item: Squadron) -> None:
|
||||||
|
item.pending_deliveries -= 1
|
||||||
|
|
||||||
|
def do_sale(self, item: Squadron) -> None:
|
||||||
|
item.untasked_aircraft -= 1
|
||||||
|
item.pending_deliveries -= 1
|
||||||
|
|
||||||
|
def do_cancel_sale(self, item: Squadron) -> None:
|
||||||
|
item.untasked_aircraft += 1
|
||||||
|
item.pending_deliveries += 1
|
||||||
|
|
||||||
|
def price_of(self, item: Squadron) -> int:
|
||||||
|
return item.aircraft.price
|
||||||
|
|
||||||
|
def name_of(self, item: Squadron, multiline: bool = False) -> str:
|
||||||
|
if multiline:
|
||||||
|
separator = "<br />"
|
||||||
|
else:
|
||||||
|
separator = " "
|
||||||
|
return separator.join([item.aircraft.name, str(item)])
|
||||||
|
|
||||||
|
|
||||||
|
class GroundUnitPurchaseAdapter(PurchaseAdapter[GroundUnitType]):
|
||||||
|
def __init__(
|
||||||
|
self, control_point: ControlPoint, coalition: Coalition, game: Game
|
||||||
|
) -> None:
|
||||||
|
super().__init__(coalition)
|
||||||
|
self.control_point = control_point
|
||||||
|
self.game = game
|
||||||
|
|
||||||
|
def pending_delivery_quantity(self, item: GroundUnitType) -> int:
|
||||||
|
return self.control_point.ground_unit_orders.pending_orders(item)
|
||||||
|
|
||||||
|
def current_quantity_of(self, item: GroundUnitType) -> int:
|
||||||
|
return self.control_point.base.total_units_of_type(item)
|
||||||
|
|
||||||
|
def can_buy(self, item: GroundUnitType) -> bool:
|
||||||
|
return super().can_buy(item) and self.control_point.has_ground_unit_source(
|
||||||
|
self.game
|
||||||
|
)
|
||||||
|
|
||||||
|
def can_sell(self, item: GroundUnitType) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def do_purchase(self, item: GroundUnitType) -> None:
|
||||||
|
self.control_point.ground_unit_orders.order({item: 1})
|
||||||
|
|
||||||
|
def do_cancel_purchase(self, item: GroundUnitType) -> None:
|
||||||
|
self.control_point.ground_unit_orders.sell({item: 1})
|
||||||
|
|
||||||
|
def do_sale(self, item: GroundUnitType) -> None:
|
||||||
|
raise TransactionError("Sale of ground units not allowed")
|
||||||
|
|
||||||
|
def do_cancel_sale(self, item: GroundUnitType) -> None:
|
||||||
|
raise TransactionError("Sale of ground units not allowed")
|
||||||
|
|
||||||
|
def price_of(self, item: GroundUnitType) -> int:
|
||||||
|
return item.price
|
||||||
|
|
||||||
|
def name_of(self, item: GroundUnitType, multiline: bool = False) -> str:
|
||||||
|
return f"{item}"
|
||||||
@ -5,12 +5,12 @@ from collections import defaultdict
|
|||||||
from typing import Sequence, Iterator, TYPE_CHECKING
|
from typing import Sequence, Iterator, TYPE_CHECKING
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
from .squadron import Squadron
|
from .squadron import Squadron
|
||||||
from ..theater import ControlPoint
|
from ..theater import ControlPoint
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
class AirWing:
|
class AirWing:
|
||||||
@ -32,11 +32,26 @@ class AirWing:
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_aircraft_types(self) -> Iterator[AircraftType]:
|
||||||
|
for aircraft, squadrons in self.squadrons.items():
|
||||||
|
for squadron in squadrons:
|
||||||
|
if squadron.untasked_aircraft:
|
||||||
|
yield aircraft
|
||||||
|
break
|
||||||
|
|
||||||
def auto_assignable_for_task(self, task: FlightType) -> Iterator[Squadron]:
|
def auto_assignable_for_task(self, task: FlightType) -> Iterator[Squadron]:
|
||||||
for squadron in self.iter_squadrons():
|
for squadron in self.iter_squadrons():
|
||||||
if squadron.can_auto_assign(task):
|
if squadron.can_auto_assign(task):
|
||||||
yield squadron
|
yield squadron
|
||||||
|
|
||||||
|
def auto_assignable_for_task_at(
|
||||||
|
self, task: FlightType, base: ControlPoint
|
||||||
|
) -> Iterator[Squadron]:
|
||||||
|
for squadron in self.iter_squadrons():
|
||||||
|
if squadron.can_auto_assign(task) and squadron.location == base:
|
||||||
|
yield squadron
|
||||||
|
|
||||||
def auto_assignable_for_task_with_type(
|
def auto_assignable_for_task_with_type(
|
||||||
self, aircraft: AircraftType, task: FlightType, base: ControlPoint
|
self, aircraft: AircraftType, task: FlightType, base: ControlPoint
|
||||||
) -> Iterator[Squadron]:
|
) -> Iterator[Squadron]:
|
||||||
@ -67,7 +82,7 @@ class AirWing:
|
|||||||
|
|
||||||
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_and_aircraft()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> int:
|
def size(self) -> int:
|
||||||
|
|||||||
@ -54,6 +54,10 @@ class Squadron:
|
|||||||
|
|
||||||
location: ControlPoint
|
location: ControlPoint
|
||||||
|
|
||||||
|
owned_aircraft: int = field(init=False, hash=False, compare=False, default=0)
|
||||||
|
untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0)
|
||||||
|
pending_deliveries: int = field(init=False, hash=False, compare=False, default=0)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
self.auto_assignable_mission_types = set(self.mission_types)
|
self.auto_assignable_mission_types = set(self.mission_types)
|
||||||
|
|
||||||
@ -62,6 +66,17 @@ class Squadron:
|
|||||||
return self.name
|
return self.name
|
||||||
return f'{self.name} "{self.nickname}"'
|
return f'{self.name} "{self.nickname}"'
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(
|
||||||
|
(
|
||||||
|
self.name,
|
||||||
|
self.nickname,
|
||||||
|
self.country,
|
||||||
|
self.role,
|
||||||
|
self.aircraft,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def player(self) -> bool:
|
def player(self) -> bool:
|
||||||
return self.coalition.player
|
return self.coalition.player
|
||||||
@ -165,8 +180,9 @@ class Squadron:
|
|||||||
if replenish_count > 0:
|
if replenish_count > 0:
|
||||||
self._recruit_pilots(replenish_count)
|
self._recruit_pilots(replenish_count)
|
||||||
|
|
||||||
def return_all_pilots(self) -> None:
|
def return_all_pilots_and_aircraft(self) -> None:
|
||||||
self.available_pilots = list(self.active_pilots)
|
self.available_pilots = list(self.active_pilots)
|
||||||
|
self.untasked_aircraft = self.owned_aircraft
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def send_on_leave(pilot: Pilot) -> None:
|
def send_on_leave(pilot: Pilot) -> None:
|
||||||
@ -238,6 +254,29 @@ class Squadron:
|
|||||||
def pilot_at_index(self, index: int) -> Pilot:
|
def pilot_at_index(self, index: int) -> Pilot:
|
||||||
return self.current_roster[index]
|
return self.current_roster[index]
|
||||||
|
|
||||||
|
def claim_inventory(self, count: int) -> None:
|
||||||
|
if self.untasked_aircraft < count:
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot remove {count} from {self.name}. Only have "
|
||||||
|
f"{self.untasked_aircraft}."
|
||||||
|
)
|
||||||
|
self.untasked_aircraft -= count
|
||||||
|
|
||||||
|
def can_fulfill_flight(self, count: int) -> bool:
|
||||||
|
return self.can_provide_pilots(count) and self.untasked_aircraft >= count
|
||||||
|
|
||||||
|
def refund_orders(self) -> None:
|
||||||
|
self.coalition.adjust_budget(self.aircraft.price * self.pending_deliveries)
|
||||||
|
self.pending_deliveries = 0
|
||||||
|
|
||||||
|
def deliver_orders(self) -> None:
|
||||||
|
self.owned_aircraft += self.pending_deliveries
|
||||||
|
self.pending_deliveries = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_fulfillable_aircraft(self) -> int:
|
||||||
|
return max(self.number_of_available_pilots, self.untasked_aircraft)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from(
|
def create_from(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import itertools
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.dcs.unittype import UnitType
|
|
||||||
|
|
||||||
BASE_MAX_STRENGTH = 1.0
|
BASE_MAX_STRENGTH = 1.0
|
||||||
BASE_MIN_STRENGTH = 0.0
|
BASE_MIN_STRENGTH = 0.0
|
||||||
@ -12,14 +6,9 @@ BASE_MIN_STRENGTH = 0.0
|
|||||||
|
|
||||||
class Base:
|
class Base:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.aircraft: dict[AircraftType, int] = {}
|
|
||||||
self.armor: dict[GroundUnitType, int] = {}
|
self.armor: dict[GroundUnitType, int] = {}
|
||||||
self.strength = 1.0
|
self.strength = 1.0
|
||||||
|
|
||||||
@property
|
|
||||||
def total_aircraft(self) -> int:
|
|
||||||
return sum(self.aircraft.values())
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_armor(self) -> int:
|
def total_armor(self) -> int:
|
||||||
return sum(self.armor.values())
|
return sum(self.armor.values())
|
||||||
@ -31,49 +20,24 @@ class Base:
|
|||||||
total += unit_type.price * count
|
total += unit_type.price * count
|
||||||
return total
|
return total
|
||||||
|
|
||||||
def total_units_of_type(self, unit_type: UnitType[Any]) -> int:
|
def total_units_of_type(self, unit_type: GroundUnitType) -> int:
|
||||||
return sum(
|
return sum([c for t, c in self.armor.items() if t == unit_type])
|
||||||
[
|
|
||||||
c
|
|
||||||
for t, c in itertools.chain(self.aircraft.items(), self.armor.items())
|
|
||||||
if t == unit_type
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def commission_units(self, units: dict[Any, int]) -> None:
|
def commission_units(self, units: dict[GroundUnitType, int]) -> None:
|
||||||
for unit_type, unit_count in units.items():
|
for unit_type, unit_count in units.items():
|
||||||
if unit_count <= 0:
|
if unit_count <= 0:
|
||||||
continue
|
continue
|
||||||
|
self.armor[unit_type] = self.armor.get(unit_type, 0) + unit_count
|
||||||
|
|
||||||
target_dict: dict[Any, int]
|
def commit_losses(self, units_lost: dict[GroundUnitType, int]) -> None:
|
||||||
if isinstance(unit_type, AircraftType):
|
|
||||||
target_dict = self.aircraft
|
|
||||||
elif isinstance(unit_type, GroundUnitType):
|
|
||||||
target_dict = self.armor
|
|
||||||
else:
|
|
||||||
logging.error(f"Unexpected unit type of {unit_type}")
|
|
||||||
return
|
|
||||||
|
|
||||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
|
||||||
|
|
||||||
def commit_losses(self, units_lost: dict[Any, int]) -> None:
|
|
||||||
for unit_type, count in units_lost.items():
|
for unit_type, count in units_lost.items():
|
||||||
target_dict: dict[Any, int]
|
if unit_type not in self.armor:
|
||||||
if unit_type in self.aircraft:
|
print("Base didn't find unit type {}".format(unit_type))
|
||||||
target_dict = self.aircraft
|
|
||||||
elif unit_type in self.armor:
|
|
||||||
target_dict = self.armor
|
|
||||||
else:
|
|
||||||
print("Base didn't find event type {}".format(unit_type))
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if unit_type not in target_dict:
|
self.armor[unit_type] = max(self.armor[unit_type] - count, 0)
|
||||||
print("Base didn't find event type {}".format(unit_type))
|
if self.armor[unit_type] == 0:
|
||||||
continue
|
del self.armor[unit_type]
|
||||||
|
|
||||||
target_dict[unit_type] = max(target_dict[unit_type] - count, 0)
|
|
||||||
if target_dict[unit_type] == 0:
|
|
||||||
del target_dict[unit_type]
|
|
||||||
|
|
||||||
def affect_strength(self, amount: float) -> None:
|
def affect_strength(self, amount: float) -> None:
|
||||||
self.strength += amount
|
self.strength += amount
|
||||||
|
|||||||
@ -317,9 +317,9 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
self.cptype = cptype
|
self.cptype = cptype
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
self.stances: Dict[int, CombatStance] = {}
|
self.stances: Dict[int, CombatStance] = {}
|
||||||
from ..unitdelivery import PendingUnitDeliveries
|
from ..groundunitorders import GroundUnitOrders
|
||||||
|
|
||||||
self.pending_unit_deliveries = PendingUnitDeliveries(self)
|
self.ground_unit_orders = GroundUnitOrders(self)
|
||||||
|
|
||||||
self.target_position: Optional[Point] = None
|
self.target_position: Optional[Point] = None
|
||||||
|
|
||||||
@ -578,25 +578,14 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
return airbase
|
return airbase
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _retreat_air_units(
|
@staticmethod
|
||||||
self, game: Game, airframe: AircraftType, count: int
|
def _retreat_squadron(squadron: Squadron) -> None:
|
||||||
) -> None:
|
logging.error("Air unit retreat not currently implemented")
|
||||||
while count:
|
|
||||||
logging.debug(f"Retreating {count} {airframe} from {self.name}")
|
|
||||||
destination = self.aircraft_retreat_destination(game, airframe)
|
|
||||||
if destination is None:
|
|
||||||
self.capture_aircraft(game, airframe, count)
|
|
||||||
return
|
|
||||||
parking = destination.unclaimed_parking(game)
|
|
||||||
transfer_amount = min([parking, count])
|
|
||||||
destination.base.commission_units({airframe: transfer_amount})
|
|
||||||
count -= transfer_amount
|
|
||||||
|
|
||||||
def retreat_air_units(self, game: Game) -> None:
|
def retreat_air_units(self, game: Game) -> None:
|
||||||
# TODO: Capture in order of price to retain maximum value?
|
# TODO: Capture in order of price to retain maximum value?
|
||||||
while self.base.aircraft:
|
for squadron in self.squadrons:
|
||||||
airframe, count = self.base.aircraft.popitem()
|
self._retreat_squadron(squadron)
|
||||||
self._retreat_air_units(game, airframe, count)
|
|
||||||
|
|
||||||
def depopulate_uncapturable_tgos(self) -> None:
|
def depopulate_uncapturable_tgos(self) -> None:
|
||||||
for tgo in self.connected_objectives:
|
for tgo in self.connected_objectives:
|
||||||
@ -605,7 +594,10 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
|
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
def capture(self, game: Game, for_player: bool) -> None:
|
def capture(self, game: Game, for_player: bool) -> None:
|
||||||
self.pending_unit_deliveries.refund_all(game.coalition_for(for_player))
|
coalition = game.coalition_for(for_player)
|
||||||
|
self.ground_unit_orders.refund_all(coalition)
|
||||||
|
for squadron in self.squadrons:
|
||||||
|
squadron.refund_orders()
|
||||||
self.retreat_ground_units(game)
|
self.retreat_ground_units(game)
|
||||||
self.retreat_air_units(game)
|
self.retreat_air_units(game)
|
||||||
self.depopulate_uncapturable_tgos()
|
self.depopulate_uncapturable_tgos()
|
||||||
@ -621,19 +613,6 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||||
...
|
...
|
||||||
|
|
||||||
def aircraft_transferring(self, game: Game) -> dict[AircraftType, int]:
|
|
||||||
ato = game.coalition_for(self.captured).ato
|
|
||||||
transferring: defaultdict[AircraftType, int] = defaultdict(int)
|
|
||||||
for package in ato.packages:
|
|
||||||
for flight in package.flights:
|
|
||||||
if flight.departure == flight.arrival:
|
|
||||||
continue
|
|
||||||
if flight.departure == self:
|
|
||||||
transferring[flight.unit_type] -= flight.count
|
|
||||||
elif flight.arrival == self:
|
|
||||||
transferring[flight.unit_type] += flight.count
|
|
||||||
return transferring
|
|
||||||
|
|
||||||
def unclaimed_parking(self, game: Game) -> int:
|
def unclaimed_parking(self, game: Game) -> int:
|
||||||
return self.total_aircraft_parking - self.allocated_aircraft(game).total
|
return self.total_aircraft_parking - self.allocated_aircraft(game).total
|
||||||
|
|
||||||
@ -663,7 +642,9 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
self.runway_status.begin_repair()
|
self.runway_status.begin_repair()
|
||||||
|
|
||||||
def process_turn(self, game: Game) -> None:
|
def process_turn(self, game: Game) -> None:
|
||||||
self.pending_unit_deliveries.process(game)
|
self.ground_unit_orders.process(game)
|
||||||
|
for squadron in self.squadrons:
|
||||||
|
squadron.deliver_orders()
|
||||||
|
|
||||||
runway_status = self.runway_status
|
runway_status = self.runway_status
|
||||||
if runway_status is not None:
|
if runway_status is not None:
|
||||||
@ -685,21 +666,22 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
u.position.x = u.position.x + delta.x
|
u.position.x = u.position.x + delta.x
|
||||||
u.position.y = u.position.y + delta.y
|
u.position.y = u.position.y + delta.y
|
||||||
|
|
||||||
def allocated_aircraft(self, game: Game) -> AircraftAllocations:
|
def allocated_aircraft(self, _game: Game) -> AircraftAllocations:
|
||||||
on_order = {}
|
present: dict[AircraftType, int] = defaultdict(int)
|
||||||
for unit_bought, count in self.pending_unit_deliveries.units.items():
|
on_order: dict[AircraftType, int] = defaultdict(int)
|
||||||
if isinstance(unit_bought, AircraftType):
|
for squadron in self.squadrons:
|
||||||
on_order[unit_bought] = count
|
present[squadron.aircraft] += squadron.owned_aircraft
|
||||||
|
# TODO: Only if this is the squadron destination, not location.
|
||||||
|
on_order[squadron.aircraft] += squadron.pending_deliveries
|
||||||
|
|
||||||
return AircraftAllocations(
|
# TODO: Implement squadron transfers.
|
||||||
self.base.aircraft, on_order, self.aircraft_transferring(game)
|
return AircraftAllocations(present, on_order, transferring={})
|
||||||
)
|
|
||||||
|
|
||||||
def allocated_ground_units(
|
def allocated_ground_units(
|
||||||
self, transfers: PendingTransfers
|
self, transfers: PendingTransfers
|
||||||
) -> GroundUnitAllocations:
|
) -> GroundUnitAllocations:
|
||||||
on_order = {}
|
on_order = {}
|
||||||
for unit_bought, count in self.pending_unit_deliveries.units.items():
|
for unit_bought, count in self.ground_unit_orders.units.items():
|
||||||
if isinstance(unit_bought, GroundUnitType):
|
if isinstance(unit_bought, GroundUnitType):
|
||||||
on_order[unit_bought] = count
|
on_order[unit_bought] = count
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,6 @@ from gen.naming import namegen
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.inventory import ControlPointAircraftInventory
|
|
||||||
from game.squadrons import Squadron
|
from game.squadrons import Squadron
|
||||||
|
|
||||||
|
|
||||||
@ -315,29 +314,20 @@ class AirliftPlanner:
|
|||||||
if cp.captured != self.for_player:
|
if cp.captured != self.for_player:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
squadrons = air_wing.auto_assignable_for_task_at(FlightType.TRANSPORT, cp)
|
||||||
for unit_type, available in inventory.all_aircraft:
|
for squadron in squadrons:
|
||||||
squadrons = air_wing.auto_assignable_for_task_with_type(
|
if self.compatible_with_mission(squadron.aircraft, cp):
|
||||||
unit_type, FlightType.TRANSPORT, cp
|
while (
|
||||||
)
|
squadron.untasked_aircraft
|
||||||
for squadron in squadrons:
|
and squadron.has_available_pilots
|
||||||
if self.compatible_with_mission(unit_type, cp):
|
and self.transfer.transport is None
|
||||||
while (
|
):
|
||||||
available
|
self.create_airlift_flight(squadron)
|
||||||
and squadron.has_available_pilots
|
|
||||||
and self.transfer.transport is None
|
|
||||||
):
|
|
||||||
flight_size = self.create_airlift_flight(
|
|
||||||
squadron, inventory
|
|
||||||
)
|
|
||||||
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) -> int:
|
||||||
self, squadron: Squadron, inventory: ControlPointAircraftInventory
|
available_aircraft = squadron.untasked_aircraft
|
||||||
) -> int:
|
|
||||||
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(
|
||||||
@ -348,8 +338,8 @@ class AirliftPlanner:
|
|||||||
# TODO: Use number_of_available_pilots directly once feature flag is gone.
|
# TODO: Use number_of_available_pilots directly once feature flag is gone.
|
||||||
# The number of currently available pilots is not relevant when pilot limits
|
# The number of currently available pilots is not relevant when pilot limits
|
||||||
# are disabled.
|
# are disabled.
|
||||||
if not squadron.can_provide_pilots(flight_size):
|
if not squadron.can_fulfill_flight(flight_size):
|
||||||
flight_size = squadron.number_of_available_pilots
|
flight_size = squadron.max_fulfillable_aircraft
|
||||||
capacity = flight_size * capacity_each
|
capacity = flight_size * capacity_each
|
||||||
|
|
||||||
if capacity < self.transfer.size:
|
if capacity < self.transfer.size:
|
||||||
@ -359,16 +349,15 @@ class AirliftPlanner:
|
|||||||
else:
|
else:
|
||||||
transfer = self.transfer
|
transfer = self.transfer
|
||||||
|
|
||||||
player = inventory.control_point.captured
|
|
||||||
flight = Flight(
|
flight = Flight(
|
||||||
self.package,
|
self.package,
|
||||||
self.game.country_for(player),
|
self.game.country_for(squadron.player),
|
||||||
squadron,
|
squadron,
|
||||||
flight_size,
|
flight_size,
|
||||||
FlightType.TRANSPORT,
|
FlightType.TRANSPORT,
|
||||||
self.game.settings.default_start_type,
|
self.game.settings.default_start_type,
|
||||||
departure=inventory.control_point,
|
departure=squadron.location,
|
||||||
arrival=inventory.control_point,
|
arrival=squadron.location,
|
||||||
divert=None,
|
divert=None,
|
||||||
cargo=transfer,
|
cargo=transfer,
|
||||||
)
|
)
|
||||||
@ -381,7 +370,6 @@ class AirliftPlanner:
|
|||||||
self.package, self.game.coalition_for(self.for_player), self.game.theater
|
self.package, self.game.coalition_for(self.for_player), self.game.theater
|
||||||
)
|
)
|
||||||
planner.populate_flight_plan(flight)
|
planner.populate_flight_plan(flight)
|
||||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
|
||||||
return flight_size
|
return flight_size
|
||||||
|
|
||||||
|
|
||||||
@ -652,8 +640,7 @@ class PendingTransfers:
|
|||||||
flight.package.remove_flight(flight)
|
flight.package.remove_flight(flight)
|
||||||
if not flight.package.flights:
|
if not flight.package.flights:
|
||||||
self.game.ato_for(self.player).remove_package(flight.package)
|
self.game.ato_for(self.player).remove_package(flight.package)
|
||||||
self.game.aircraft_inventory.return_from_flight(flight)
|
flight.return_pilots_and_aircraft()
|
||||||
flight.clear_roster()
|
|
||||||
|
|
||||||
@cancel_transport.register
|
@cancel_transport.register
|
||||||
def _cancel_transport_convoy(
|
def _cancel_transport_convoy(
|
||||||
@ -756,16 +743,12 @@ class PendingTransfers:
|
|||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def current_airlift_capacity(self, control_point: ControlPoint) -> int:
|
@staticmethod
|
||||||
inventory = self.game.aircraft_inventory.for_control_point(control_point)
|
def current_airlift_capacity(control_point: ControlPoint) -> int:
|
||||||
squadrons = self.game.air_wing_for(
|
|
||||||
control_point.captured
|
|
||||||
).auto_assignable_for_task(FlightType.TRANSPORT)
|
|
||||||
unit_types = {s.aircraft for s in squadrons}
|
|
||||||
return sum(
|
return sum(
|
||||||
count
|
s.owned_aircraft
|
||||||
for unit_type, count in inventory.all_aircraft
|
for s in control_point.squadrons
|
||||||
if unit_type in unit_types
|
if s.can_auto_assign(FlightType.TRANSPORT)
|
||||||
)
|
)
|
||||||
|
|
||||||
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
|
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
|
||||||
|
|||||||
@ -108,7 +108,7 @@ from .naming import namegen
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.squadrons import Pilot
|
from game.squadrons import Pilot, Squadron
|
||||||
|
|
||||||
WARM_START_HELI_ALT = meters(500)
|
WARM_START_HELI_ALT = meters(500)
|
||||||
WARM_START_ALTITUDE = meters(3000)
|
WARM_START_ALTITUDE = meters(3000)
|
||||||
@ -594,8 +594,7 @@ class AircraftConflictGenerator:
|
|||||||
def spawn_unused_aircraft(
|
def spawn_unused_aircraft(
|
||||||
self, player_country: Country, enemy_country: Country
|
self, player_country: Country, enemy_country: Country
|
||||||
) -> None:
|
) -> None:
|
||||||
inventories = self.game.aircraft_inventory.inventories
|
for control_point in self.game.theater.controlpoints:
|
||||||
for control_point, inventory in inventories.items():
|
|
||||||
if not isinstance(control_point, Airfield):
|
if not isinstance(control_point, Airfield):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -605,11 +604,9 @@ class AircraftConflictGenerator:
|
|||||||
else:
|
else:
|
||||||
country = enemy_country
|
country = enemy_country
|
||||||
|
|
||||||
for aircraft, available in inventory.all_aircraft:
|
for squadron in control_point.squadrons:
|
||||||
try:
|
try:
|
||||||
self._spawn_unused_at(
|
self._spawn_unused_at(control_point, country, faction, squadron)
|
||||||
control_point, country, faction, aircraft, available
|
|
||||||
)
|
|
||||||
except NoParkingSlotError:
|
except NoParkingSlotError:
|
||||||
# If we run out of parking, stop spawning aircraft.
|
# If we run out of parking, stop spawning aircraft.
|
||||||
return
|
return
|
||||||
@ -619,17 +616,16 @@ class AircraftConflictGenerator:
|
|||||||
control_point: Airfield,
|
control_point: Airfield,
|
||||||
country: Country,
|
country: Country,
|
||||||
faction: Faction,
|
faction: Faction,
|
||||||
aircraft: AircraftType,
|
squadron: Squadron,
|
||||||
number: int,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
for _ in range(number):
|
for _ in range(squadron.untasked_aircraft):
|
||||||
# Creating a flight even those this isn't a fragged mission lets us
|
# Creating a flight even those this isn't a fragged mission lets us
|
||||||
# reuse the existing debriefing code.
|
# reuse the existing debriefing code.
|
||||||
# TODO: Special flight type?
|
# TODO: Special flight type?
|
||||||
flight = Flight(
|
flight = Flight(
|
||||||
Package(control_point),
|
Package(control_point),
|
||||||
faction.country,
|
faction.country,
|
||||||
self.game.air_wing_for(control_point.captured).squadron_for(aircraft),
|
squadron,
|
||||||
1,
|
1,
|
||||||
FlightType.BARCAP,
|
FlightType.BARCAP,
|
||||||
"Cold",
|
"Cold",
|
||||||
@ -641,16 +637,13 @@ class AircraftConflictGenerator:
|
|||||||
group = self._generate_at_airport(
|
group = self._generate_at_airport(
|
||||||
name=namegen.next_aircraft_name(country, control_point.id, flight),
|
name=namegen.next_aircraft_name(country, control_point.id, flight),
|
||||||
side=country,
|
side=country,
|
||||||
unit_type=aircraft.dcs_unit_type,
|
unit_type=squadron.aircraft.dcs_unit_type,
|
||||||
count=1,
|
count=1,
|
||||||
start_type="Cold",
|
start_type="Cold",
|
||||||
airport=control_point.airport,
|
airport=control_point.airport,
|
||||||
)
|
)
|
||||||
|
|
||||||
if aircraft in faction.liveries_overrides:
|
self._setup_livery(flight, group)
|
||||||
livery = random.choice(faction.liveries_overrides[aircraft])
|
|
||||||
for unit in group.units:
|
|
||||||
unit.livery_id = livery
|
|
||||||
|
|
||||||
group.uncontrolled = True
|
group.uncontrolled = True
|
||||||
self.unit_map.add_aircraft(group, flight)
|
self.unit_map.add_aircraft(group, flight)
|
||||||
|
|||||||
@ -290,6 +290,7 @@ class Flight:
|
|||||||
self.package = package
|
self.package = package
|
||||||
self.country = country
|
self.country = country
|
||||||
self.squadron = squadron
|
self.squadron = squadron
|
||||||
|
self.squadron.claim_inventory(count)
|
||||||
if roster is None:
|
if roster is None:
|
||||||
self.roster = FlightRoster(self.squadron, initial_size=count)
|
self.roster = FlightRoster(self.squadron, initial_size=count)
|
||||||
else:
|
else:
|
||||||
@ -338,6 +339,7 @@ class Flight:
|
|||||||
return self.flight_plan.waypoints[1:]
|
return self.flight_plan.waypoints[1:]
|
||||||
|
|
||||||
def resize(self, new_size: int) -> None:
|
def resize(self, new_size: int) -> None:
|
||||||
|
self.squadron.claim_inventory(new_size - self.count)
|
||||||
self.roster.resize(new_size)
|
self.roster.resize(new_size)
|
||||||
|
|
||||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
||||||
@ -347,8 +349,9 @@ class Flight:
|
|||||||
def missing_pilots(self) -> int:
|
def missing_pilots(self) -> int:
|
||||||
return self.roster.missing_pilots
|
return self.roster.missing_pilots
|
||||||
|
|
||||||
def clear_roster(self) -> None:
|
def return_pilots_and_aircraft(self) -> None:
|
||||||
self.roster.clear()
|
self.roster.clear()
|
||||||
|
self.squadron.claim_inventory(-self.count)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
if self.custom_name:
|
if self.custom_name:
|
||||||
|
|||||||
@ -165,8 +165,7 @@ class PackageModel(QAbstractListModel):
|
|||||||
self.beginRemoveRows(QModelIndex(), index, index)
|
self.beginRemoveRows(QModelIndex(), index, index)
|
||||||
if flight.cargo is not None:
|
if flight.cargo is not None:
|
||||||
flight.cargo.transport = None
|
flight.cargo.transport = None
|
||||||
self.game_model.game.aircraft_inventory.return_from_flight(flight)
|
flight.return_pilots_and_aircraft()
|
||||||
flight.clear_roster()
|
|
||||||
self.package.remove_flight(flight)
|
self.package.remove_flight(flight)
|
||||||
self.endRemoveRows()
|
self.endRemoveRows()
|
||||||
self.update_tot()
|
self.update_tot()
|
||||||
@ -258,8 +257,7 @@ class AtoModel(QAbstractListModel):
|
|||||||
self.beginRemoveRows(QModelIndex(), index, index)
|
self.beginRemoveRows(QModelIndex(), index, index)
|
||||||
self.ato.remove_package(package)
|
self.ato.remove_package(package)
|
||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
self.game.aircraft_inventory.return_from_flight(flight)
|
flight.return_pilots_and_aircraft()
|
||||||
flight.clear_roster()
|
|
||||||
if flight.cargo is not None:
|
if flight.cargo is not None:
|
||||||
flight.cargo.transport = None
|
flight.cargo.transport = None
|
||||||
self.endRemoveRows()
|
self.endRemoveRows()
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
"""Combo box for selecting a departure airfield."""
|
|
||||||
from typing import Iterable, Optional
|
|
||||||
|
|
||||||
from PySide2.QtCore import Signal
|
|
||||||
from PySide2.QtWidgets import QComboBox
|
|
||||||
from dcs.unittype import FlyingType
|
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
|
||||||
from game.inventory import GlobalAircraftInventory
|
|
||||||
from game.theater.controlpoint import ControlPoint
|
|
||||||
|
|
||||||
|
|
||||||
class QOriginAirfieldSelector(QComboBox):
|
|
||||||
"""A combo box for selecting a flight's departure airfield.
|
|
||||||
|
|
||||||
The combo box will automatically be populated with all departure airfields
|
|
||||||
that have unassigned inventory of the given aircraft type.
|
|
||||||
"""
|
|
||||||
|
|
||||||
availability_changed = Signal(int)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
global_inventory: GlobalAircraftInventory,
|
|
||||||
origins: Iterable[ControlPoint],
|
|
||||||
aircraft: Optional[AircraftType],
|
|
||||||
) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.global_inventory = global_inventory
|
|
||||||
self.origins = list(origins)
|
|
||||||
self.aircraft = aircraft
|
|
||||||
self.rebuild_selector()
|
|
||||||
self.currentIndexChanged.connect(self.index_changed)
|
|
||||||
self.setSizeAdjustPolicy(self.AdjustToContents)
|
|
||||||
|
|
||||||
def change_aircraft(self, aircraft: Optional[FlyingType]) -> None:
|
|
||||||
if self.aircraft == aircraft:
|
|
||||||
return
|
|
||||||
self.aircraft = aircraft
|
|
||||||
self.rebuild_selector()
|
|
||||||
|
|
||||||
def rebuild_selector(self) -> None:
|
|
||||||
self.clear()
|
|
||||||
if self.aircraft is None:
|
|
||||||
return
|
|
||||||
for origin in self.origins:
|
|
||||||
if not origin.can_operate(self.aircraft):
|
|
||||||
continue
|
|
||||||
|
|
||||||
inventory = self.global_inventory.for_control_point(origin)
|
|
||||||
available = inventory.available(self.aircraft)
|
|
||||||
if available:
|
|
||||||
self.addItem(f"{origin.name} ({available} available)", origin)
|
|
||||||
self.model().sort(0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> int:
|
|
||||||
origin = self.currentData()
|
|
||||||
if origin is None:
|
|
||||||
return 0
|
|
||||||
inventory = self.global_inventory.for_control_point(origin)
|
|
||||||
return inventory.available(self.aircraft)
|
|
||||||
|
|
||||||
def index_changed(self, index: int) -> None:
|
|
||||||
origin = self.itemData(index)
|
|
||||||
if origin is None:
|
|
||||||
return
|
|
||||||
inventory = self.global_inventory.for_control_point(origin)
|
|
||||||
self.availability_changed.emit(inventory.available(self.aircraft))
|
|
||||||
@ -122,14 +122,6 @@ class SquadronBaseSelector(QComboBox):
|
|||||||
self.model().sort(0)
|
self.model().sort(0)
|
||||||
self.setCurrentText(self.squadron.location.name)
|
self.setCurrentText(self.squadron.location.name)
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> int:
|
|
||||||
origin = self.currentData()
|
|
||||||
if origin is None:
|
|
||||||
return 0
|
|
||||||
inventory = self.global_inventory.for_control_point(origin)
|
|
||||||
return inventory.available(self.aircraft)
|
|
||||||
|
|
||||||
|
|
||||||
class SquadronConfigurationBox(QGroupBox):
|
class SquadronConfigurationBox(QGroupBox):
|
||||||
def __init__(self, squadron: Squadron, theater: ConflictTheater) -> None:
|
def __init__(self, squadron: Squadron, theater: ConflictTheater) -> None:
|
||||||
|
|||||||
@ -16,7 +16,6 @@ from PySide2.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from game.inventory import ControlPointAircraftInventory
|
|
||||||
from game.squadrons import Squadron
|
from game.squadrons import Squadron
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from qt_ui.delegates import TwoColumnRowDelegate
|
from qt_ui.delegates import TwoColumnRowDelegate
|
||||||
@ -127,19 +126,13 @@ class AircraftInventoryData:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def each_from_inventory(
|
def each_untasked_from_squadron(
|
||||||
cls, inventory: ControlPointAircraftInventory
|
cls, squadron: Squadron
|
||||||
) -> Iterator[AircraftInventoryData]:
|
) -> Iterator[AircraftInventoryData]:
|
||||||
for unit_type, num_units in inventory.all_aircraft:
|
for _ in range(0, squadron.untasked_aircraft):
|
||||||
for _ in range(0, num_units):
|
yield AircraftInventoryData(
|
||||||
yield AircraftInventoryData(
|
squadron.name, squadron.aircraft.name, "Idle", "N/A", "N/A", "N/A"
|
||||||
inventory.control_point.name,
|
)
|
||||||
unit_type.name,
|
|
||||||
"Idle",
|
|
||||||
"N/A",
|
|
||||||
"N/A",
|
|
||||||
"N/A",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AirInventoryView(QWidget):
|
class AirInventoryView(QWidget):
|
||||||
@ -188,9 +181,8 @@ class AirInventoryView(QWidget):
|
|||||||
|
|
||||||
def iter_unallocated_aircraft(self) -> Iterator[AircraftInventoryData]:
|
def iter_unallocated_aircraft(self) -> Iterator[AircraftInventoryData]:
|
||||||
game = self.game_model.game
|
game = self.game_model.game
|
||||||
for control_point, inventory in game.aircraft_inventory.inventories.items():
|
for squadron in game.blue.air_wing.iter_squadrons():
|
||||||
if control_point.captured:
|
yield from AircraftInventoryData.each_untasked_from_squadron(squadron)
|
||||||
yield from AircraftInventoryData.each_from_inventory(inventory)
|
|
||||||
|
|
||||||
def get_data(self, only_unallocated: bool) -> Iterator[AircraftInventoryData]:
|
def get_data(self, only_unallocated: bool) -> Iterator[AircraftInventoryData]:
|
||||||
yield from self.iter_unallocated_aircraft()
|
yield from self.iter_unallocated_aircraft()
|
||||||
|
|||||||
@ -24,7 +24,7 @@ from qt_ui.uiconstants import EVENT_ICONS
|
|||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.basemenu.NewUnitTransferDialog import NewUnitTransferDialog
|
from qt_ui.windows.basemenu.NewUnitTransferDialog import NewUnitTransferDialog
|
||||||
from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs
|
from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs
|
||||||
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
|
from qt_ui.windows.basemenu.UnitTransactionFrame import UnitTransactionFrame
|
||||||
|
|
||||||
|
|
||||||
class QBaseMenu2(QDialog):
|
class QBaseMenu2(QDialog):
|
||||||
@ -108,7 +108,7 @@ class QBaseMenu2(QDialog):
|
|||||||
capture_button.clicked.connect(self.cheat_capture)
|
capture_button.clicked.connect(self.cheat_capture)
|
||||||
|
|
||||||
self.budget_display = QLabel(
|
self.budget_display = QLabel(
|
||||||
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.blue.budget)
|
UnitTransactionFrame.BUDGET_FORMAT.format(self.game_model.game.blue.budget)
|
||||||
)
|
)
|
||||||
self.budget_display.setAlignment(Qt.AlignRight | Qt.AlignBottom)
|
self.budget_display.setAlignment(Qt.AlignRight | Qt.AlignBottom)
|
||||||
self.budget_display.setProperty("style", "budget-label")
|
self.budget_display.setProperty("style", "budget-label")
|
||||||
@ -190,7 +190,7 @@ class QBaseMenu2(QDialog):
|
|||||||
self.repair_button.setDisabled(True)
|
self.repair_button.setDisabled(True)
|
||||||
|
|
||||||
def update_intel_summary(self) -> None:
|
def update_intel_summary(self) -> None:
|
||||||
aircraft = self.cp.base.total_aircraft
|
aircraft = self.cp.allocated_aircraft(self.game_model.game).total_present
|
||||||
parking = self.cp.total_aircraft_parking
|
parking = self.cp.total_aircraft_parking
|
||||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||||
deployable_unit_info = ""
|
deployable_unit_info = ""
|
||||||
@ -258,5 +258,5 @@ class QBaseMenu2(QDialog):
|
|||||||
|
|
||||||
def update_budget(self, game: Game) -> None:
|
def update_budget(self, game: Game) -> None:
|
||||||
self.budget_display.setText(
|
self.budget_display.setText(
|
||||||
QRecruitBehaviour.BUDGET_FORMAT.format(game.blue.budget)
|
UnitTransactionFrame.BUDGET_FORMAT.format(game.blue.budget)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from enum import Enum
|
||||||
|
from typing import TypeVar, Generic
|
||||||
|
|
||||||
from PySide2.QtCore import Qt
|
from PySide2.QtCore import Qt
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
@ -11,15 +14,15 @@ from PySide2.QtWidgets import (
|
|||||||
QSpacerItem,
|
QSpacerItem,
|
||||||
QGridLayout,
|
QGridLayout,
|
||||||
QApplication,
|
QApplication,
|
||||||
|
QFrame,
|
||||||
|
QMessageBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
from game.dcs.unittype import UnitType
|
from game.dcs.unittype import UnitType
|
||||||
from game.theater import ControlPoint
|
|
||||||
from game.unitdelivery import PendingUnitDeliveries
|
|
||||||
from qt_ui.models import GameModel
|
from qt_ui.models import GameModel
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.QUnitInfoWindow import QUnitInfoWindow
|
from qt_ui.windows.QUnitInfoWindow import QUnitInfoWindow
|
||||||
from enum import Enum
|
from game.purchaseadapter import PurchaseAdapter, TransactionError
|
||||||
|
|
||||||
|
|
||||||
class RecruitType(Enum):
|
class RecruitType(Enum):
|
||||||
@ -27,21 +30,28 @@ class RecruitType(Enum):
|
|||||||
SELL = 1
|
SELL = 1
|
||||||
|
|
||||||
|
|
||||||
class PurchaseGroup(QGroupBox):
|
TransactionItemType = TypeVar("TransactionItemType")
|
||||||
def __init__(self, unit_type: UnitType, recruiter: QRecruitBehaviour) -> None:
|
|
||||||
|
|
||||||
|
class PurchaseGroup(QGroupBox, Generic[TransactionItemType]):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
item: TransactionItemType,
|
||||||
|
recruiter: UnitTransactionFrame[TransactionItemType],
|
||||||
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.unit_type = unit_type
|
self.item = item
|
||||||
self.recruiter = recruiter
|
self.recruiter = recruiter
|
||||||
|
|
||||||
self.setProperty("style", "buy-box")
|
self.setProperty("style", "buy-box")
|
||||||
self.setMaximumHeight(36)
|
self.setMaximumHeight(72)
|
||||||
self.setMinimumHeight(36)
|
self.setMinimumHeight(36)
|
||||||
layout = QHBoxLayout()
|
layout = QHBoxLayout()
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
self.sell_button = QPushButton("-")
|
self.sell_button = QPushButton("-")
|
||||||
self.sell_button.setProperty("style", "btn-sell")
|
self.sell_button.setProperty("style", "btn-sell")
|
||||||
self.sell_button.setDisabled(not recruiter.enable_sale(unit_type))
|
self.sell_button.setDisabled(not recruiter.enable_sale(item))
|
||||||
self.sell_button.setMinimumSize(16, 16)
|
self.sell_button.setMinimumSize(16, 16)
|
||||||
self.sell_button.setMaximumSize(16, 16)
|
self.sell_button.setMaximumSize(16, 16)
|
||||||
self.sell_button.setSizePolicy(
|
self.sell_button.setSizePolicy(
|
||||||
@ -49,7 +59,7 @@ class PurchaseGroup(QGroupBox):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.sell_button.clicked.connect(
|
self.sell_button.clicked.connect(
|
||||||
lambda: self.recruiter.recruit_handler(RecruitType.SELL, self.unit_type)
|
lambda: self.recruiter.recruit_handler(RecruitType.SELL, self.item)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.amount_bought = QLabel()
|
self.amount_bought = QLabel()
|
||||||
@ -59,12 +69,12 @@ class PurchaseGroup(QGroupBox):
|
|||||||
|
|
||||||
self.buy_button = QPushButton("+")
|
self.buy_button = QPushButton("+")
|
||||||
self.buy_button.setProperty("style", "btn-buy")
|
self.buy_button.setProperty("style", "btn-buy")
|
||||||
self.buy_button.setDisabled(not recruiter.enable_purchase(unit_type))
|
self.buy_button.setDisabled(not recruiter.enable_purchase(item))
|
||||||
self.buy_button.setMinimumSize(16, 16)
|
self.buy_button.setMinimumSize(16, 16)
|
||||||
self.buy_button.setMaximumSize(16, 16)
|
self.buy_button.setMaximumSize(16, 16)
|
||||||
|
|
||||||
self.buy_button.clicked.connect(
|
self.buy_button.clicked.connect(
|
||||||
lambda: self.recruiter.recruit_handler(RecruitType.BUY, self.unit_type)
|
lambda: self.recruiter.recruit_handler(RecruitType.BUY, self.item)
|
||||||
)
|
)
|
||||||
self.buy_button.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
self.buy_button.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||||
|
|
||||||
@ -76,36 +86,53 @@ class PurchaseGroup(QGroupBox):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def pending_units(self) -> int:
|
def pending_units(self) -> int:
|
||||||
return self.recruiter.pending_deliveries.units.get(self.unit_type, 0)
|
return self.recruiter.pending_delivery_quantity(self.item)
|
||||||
|
|
||||||
def update_state(self) -> None:
|
def update_state(self) -> None:
|
||||||
self.buy_button.setEnabled(self.recruiter.enable_purchase(self.unit_type))
|
self.buy_button.setEnabled(self.recruiter.enable_purchase(self.item))
|
||||||
self.buy_button.setToolTip(
|
self.buy_button.setToolTip(
|
||||||
self.recruiter.purchase_tooltip(self.buy_button.isEnabled())
|
self.recruiter.purchase_tooltip(self.buy_button.isEnabled())
|
||||||
)
|
)
|
||||||
self.sell_button.setEnabled(self.recruiter.enable_sale(self.unit_type))
|
self.sell_button.setEnabled(self.recruiter.enable_sale(self.item))
|
||||||
self.sell_button.setToolTip(
|
self.sell_button.setToolTip(
|
||||||
self.recruiter.sell_tooltip(self.sell_button.isEnabled())
|
self.recruiter.sell_tooltip(self.sell_button.isEnabled())
|
||||||
)
|
)
|
||||||
self.amount_bought.setText(f"<b>{self.pending_units}</b>")
|
self.amount_bought.setText(f"<b>{self.pending_units}</b>")
|
||||||
|
|
||||||
|
|
||||||
class QRecruitBehaviour:
|
class UnitTransactionFrame(QFrame, Generic[TransactionItemType]):
|
||||||
game_model: GameModel
|
|
||||||
cp: ControlPoint
|
|
||||||
purchase_groups: dict[UnitType, PurchaseGroup]
|
|
||||||
existing_units_labels = None
|
|
||||||
maximum_units = -1
|
|
||||||
BUDGET_FORMAT = "Available Budget: <b>${:.2f}M</b>"
|
BUDGET_FORMAT = "Available Budget: <b>${:.2f}M</b>"
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
game_model: GameModel,
|
||||||
|
purchase_adapter: PurchaseAdapter[TransactionItemType],
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.game_model = game_model
|
||||||
|
self.purchase_adapter = purchase_adapter
|
||||||
self.existing_units_labels = {}
|
self.existing_units_labels = {}
|
||||||
self.purchase_groups = {}
|
self.purchase_groups: dict[
|
||||||
|
TransactionItemType, PurchaseGroup[TransactionItemType]
|
||||||
|
] = {}
|
||||||
self.update_available_budget()
|
self.update_available_budget()
|
||||||
|
|
||||||
@property
|
def current_quantity_of(self, item: TransactionItemType) -> int:
|
||||||
def pending_deliveries(self) -> PendingUnitDeliveries:
|
return self.purchase_adapter.current_quantity_of(item)
|
||||||
return self.cp.pending_unit_deliveries
|
|
||||||
|
def pending_delivery_quantity(self, item: TransactionItemType) -> int:
|
||||||
|
return self.purchase_adapter.pending_delivery_quantity(item)
|
||||||
|
|
||||||
|
def expected_quantity_next_turn(self, item: TransactionItemType) -> int:
|
||||||
|
return self.purchase_adapter.expected_quantity_next_turn(item)
|
||||||
|
|
||||||
|
def display_name_of(
|
||||||
|
self, item: TransactionItemType, multiline: bool = False
|
||||||
|
) -> str:
|
||||||
|
return self.purchase_adapter.name_of(item, multiline)
|
||||||
|
|
||||||
|
def price_of(self, item: TransactionItemType) -> int:
|
||||||
|
return self.purchase_adapter.price_of(item)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def budget(self) -> float:
|
def budget(self) -> float:
|
||||||
@ -117,20 +144,20 @@ class QRecruitBehaviour:
|
|||||||
|
|
||||||
def add_purchase_row(
|
def add_purchase_row(
|
||||||
self,
|
self,
|
||||||
unit_type: UnitType,
|
item: TransactionItemType,
|
||||||
layout: QGridLayout,
|
layout: QGridLayout,
|
||||||
row: int,
|
row: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
exist = QGroupBox()
|
exist = QGroupBox()
|
||||||
exist.setProperty("style", "buy-box")
|
exist.setProperty("style", "buy-box")
|
||||||
exist.setMaximumHeight(36)
|
exist.setMaximumHeight(72)
|
||||||
exist.setMinimumHeight(36)
|
exist.setMinimumHeight(36)
|
||||||
existLayout = QHBoxLayout()
|
existLayout = QHBoxLayout()
|
||||||
exist.setLayout(existLayout)
|
exist.setLayout(existLayout)
|
||||||
|
|
||||||
existing_units = self.cp.base.total_units_of_type(unit_type)
|
existing_units = self.current_quantity_of(item)
|
||||||
|
|
||||||
unitName = QLabel(f"<b>{unit_type.name}</b>")
|
unitName = QLabel(f"<b>{self.display_name_of(item, multiline=True)}</b>")
|
||||||
unitName.setSizePolicy(
|
unitName.setSizePolicy(
|
||||||
QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
)
|
)
|
||||||
@ -138,17 +165,17 @@ class QRecruitBehaviour:
|
|||||||
existing_units = QLabel(str(existing_units))
|
existing_units = QLabel(str(existing_units))
|
||||||
existing_units.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
existing_units.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||||
|
|
||||||
self.existing_units_labels[unit_type] = existing_units
|
self.existing_units_labels[item] = existing_units
|
||||||
|
|
||||||
price = QLabel(f"<b>$ {unit_type.price}</b> M")
|
price = QLabel(f"<b>$ {self.price_of(item)}</b> M")
|
||||||
price.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
price.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||||
|
|
||||||
purchase_group = PurchaseGroup(unit_type, self)
|
purchase_group = PurchaseGroup(item, self)
|
||||||
self.purchase_groups[unit_type] = purchase_group
|
self.purchase_groups[item] = purchase_group
|
||||||
|
|
||||||
info = QGroupBox()
|
info = QGroupBox()
|
||||||
info.setProperty("style", "buy-box")
|
info.setProperty("style", "buy-box")
|
||||||
info.setMaximumHeight(36)
|
info.setMaximumHeight(72)
|
||||||
info.setMinimumHeight(36)
|
info.setMinimumHeight(36)
|
||||||
infolayout = QHBoxLayout()
|
infolayout = QHBoxLayout()
|
||||||
info.setLayout(infolayout)
|
info.setLayout(infolayout)
|
||||||
@ -157,7 +184,7 @@ class QRecruitBehaviour:
|
|||||||
unitInfo.setProperty("style", "btn-info")
|
unitInfo.setProperty("style", "btn-info")
|
||||||
unitInfo.setMinimumSize(16, 16)
|
unitInfo.setMinimumSize(16, 16)
|
||||||
unitInfo.setMaximumSize(16, 16)
|
unitInfo.setMaximumSize(16, 16)
|
||||||
unitInfo.clicked.connect(lambda: self.info(unit_type))
|
unitInfo.clicked.connect(lambda: self.info(item))
|
||||||
unitInfo.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
unitInfo.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||||
|
|
||||||
existLayout.addWidget(unitName)
|
existLayout.addWidget(unitName)
|
||||||
@ -179,7 +206,9 @@ class QRecruitBehaviour:
|
|||||||
def update_available_budget(self) -> None:
|
def update_available_budget(self) -> None:
|
||||||
GameUpdateSignal.get_instance().updateBudget(self.game_model.game)
|
GameUpdateSignal.get_instance().updateBudget(self.game_model.game)
|
||||||
|
|
||||||
def recruit_handler(self, recruit_type: RecruitType, unit_type: UnitType) -> None:
|
def recruit_handler(
|
||||||
|
self, recruit_type: RecruitType, item: TransactionItemType
|
||||||
|
) -> None:
|
||||||
# Lookup if Keyboard Modifiers were pressed
|
# Lookup if Keyboard Modifiers were pressed
|
||||||
# Shift = 10 times
|
# Shift = 10 times
|
||||||
# CTRL = 5 Times
|
# CTRL = 5 Times
|
||||||
@ -191,51 +220,54 @@ class QRecruitBehaviour:
|
|||||||
else:
|
else:
|
||||||
amount = 1
|
amount = 1
|
||||||
|
|
||||||
for i in range(amount):
|
if recruit_type == RecruitType.SELL:
|
||||||
if recruit_type == RecruitType.SELL:
|
self.sell(item, amount)
|
||||||
if not self.sell(unit_type):
|
elif recruit_type == RecruitType.BUY:
|
||||||
return
|
self.buy(item, amount)
|
||||||
elif recruit_type == RecruitType.BUY:
|
|
||||||
if not self.buy(unit_type):
|
|
||||||
return
|
|
||||||
|
|
||||||
def buy(self, unit_type: UnitType) -> bool:
|
def post_transaction_update(self) -> None:
|
||||||
|
|
||||||
if not self.enable_purchase(unit_type):
|
|
||||||
logging.error(f"Purchase of {unit_type} not allowed at {self.cp.name}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.pending_deliveries.order({unit_type: 1})
|
|
||||||
self.budget -= unit_type.price
|
|
||||||
self.update_purchase_controls()
|
self.update_purchase_controls()
|
||||||
self.update_available_budget()
|
self.update_available_budget()
|
||||||
|
|
||||||
|
def buy(self, item: TransactionItemType, quantity: int) -> bool:
|
||||||
|
try:
|
||||||
|
self.purchase_adapter.buy(item, quantity)
|
||||||
|
except TransactionError as ex:
|
||||||
|
logging.exception(f"Purchase of {self.display_name_of(item)} failed")
|
||||||
|
QMessageBox.warning(self, "Purchase failed", str(ex), QMessageBox.Ok)
|
||||||
|
return False
|
||||||
|
self.post_transaction_update()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def sell(self, unit_type: UnitType) -> bool:
|
def sell(self, item: TransactionItemType, quantity: int) -> bool:
|
||||||
if self.pending_deliveries.available_next_turn(unit_type) > 0:
|
try:
|
||||||
self.budget += unit_type.price
|
self.purchase_adapter.sell(item, quantity)
|
||||||
self.pending_deliveries.sell({unit_type: 1})
|
except TransactionError as ex:
|
||||||
self.update_purchase_controls()
|
logging.exception(f"Sale of {self.display_name_of(item)} failed")
|
||||||
self.update_available_budget()
|
QMessageBox.warning(self, "Sale failed", str(ex), QMessageBox.Ok)
|
||||||
|
return False
|
||||||
|
self.post_transaction_update()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def update_purchase_controls(self) -> None:
|
def update_purchase_controls(self) -> None:
|
||||||
for group in self.purchase_groups.values():
|
for group in self.purchase_groups.values():
|
||||||
group.update_state()
|
group.update_state()
|
||||||
|
|
||||||
def enable_purchase(self, unit_type: UnitType) -> bool:
|
def enable_purchase(self, item: TransactionItemType) -> bool:
|
||||||
return self.budget >= unit_type.price
|
return self.purchase_adapter.can_buy(item)
|
||||||
|
|
||||||
def enable_sale(self, unit_type: UnitType) -> bool:
|
def enable_sale(self, item: TransactionItemType) -> bool:
|
||||||
return True
|
return self.purchase_adapter.can_sell_or_cancel(item)
|
||||||
|
|
||||||
def purchase_tooltip(self, is_enabled: bool) -> str:
|
@staticmethod
|
||||||
|
def purchase_tooltip(is_enabled: bool) -> str:
|
||||||
if is_enabled:
|
if is_enabled:
|
||||||
return "Buy unit. Use Shift or Ctrl key to buy multiple units at once."
|
return "Buy unit. Use Shift or Ctrl key to buy multiple units at once."
|
||||||
else:
|
else:
|
||||||
return "Unit can not be bought."
|
return "Unit can not be bought."
|
||||||
|
|
||||||
def sell_tooltip(self, is_enabled: bool) -> str:
|
@staticmethod
|
||||||
|
def sell_tooltip(is_enabled: bool) -> str:
|
||||||
if is_enabled:
|
if is_enabled:
|
||||||
return "Sell unit. Use Shift or Ctrl key to buy multiple units at once."
|
return "Sell unit. Use Shift or Ctrl key to buy multiple units at once."
|
||||||
else:
|
else:
|
||||||
@ -244,9 +276,3 @@ class QRecruitBehaviour:
|
|||||||
def info(self, unit_type: UnitType) -> None:
|
def info(self, unit_type: UnitType) -> None:
|
||||||
self.info_window = QUnitInfoWindow(self.game_model.game, unit_type)
|
self.info_window = QUnitInfoWindow(self.game_model.game, unit_type)
|
||||||
self.info_window.show()
|
self.info_window.show()
|
||||||
|
|
||||||
def set_maximum_units(self, maximum_units):
|
|
||||||
"""
|
|
||||||
Set the maximum number of units that can be bought
|
|
||||||
"""
|
|
||||||
self.maximum_units = maximum_units
|
|
||||||
@ -1,38 +1,38 @@
|
|||||||
import logging
|
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
from PySide2.QtCore import Qt
|
from PySide2.QtCore import Qt
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QFrame,
|
|
||||||
QGridLayout,
|
QGridLayout,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QLabel,
|
QLabel,
|
||||||
QMessageBox,
|
|
||||||
QScrollArea,
|
QScrollArea,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
from dcs.helicopters import helicopter_map
|
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.theater import ControlPoint, ControlPointType
|
from game.squadrons import Squadron
|
||||||
|
from game.theater import ControlPoint
|
||||||
from qt_ui.models import GameModel
|
from qt_ui.models import GameModel
|
||||||
from qt_ui.uiconstants import ICONS
|
from qt_ui.uiconstants import ICONS
|
||||||
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
|
from qt_ui.windows.basemenu.UnitTransactionFrame import UnitTransactionFrame
|
||||||
|
from game.purchaseadapter import AircraftPurchaseAdapter
|
||||||
|
|
||||||
|
|
||||||
class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
class QAircraftRecruitmentMenu(UnitTransactionFrame[Squadron]):
|
||||||
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
|
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
|
||||||
QFrame.__init__(self)
|
super().__init__(
|
||||||
|
game_model,
|
||||||
|
AircraftPurchaseAdapter(
|
||||||
|
cp, game_model.game.coalition_for(cp.captured), game_model.game
|
||||||
|
),
|
||||||
|
)
|
||||||
self.cp = cp
|
self.cp = cp
|
||||||
self.game_model = game_model
|
self.game_model = game_model
|
||||||
self.purchase_groups = {}
|
self.purchase_groups = {}
|
||||||
self.bought_amount_labels = {}
|
self.bought_amount_labels = {}
|
||||||
self.existing_units_labels = {}
|
self.existing_units_labels = {}
|
||||||
|
|
||||||
# Determine maximum number of aircrafts that can be bought
|
|
||||||
self.set_maximum_units(self.cp.total_aircraft_parking)
|
|
||||||
|
|
||||||
self.bought_amount_labels = {}
|
self.bought_amount_labels = {}
|
||||||
self.existing_units_labels = {}
|
self.existing_units_labels = {}
|
||||||
|
|
||||||
@ -48,9 +48,9 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
|||||||
for squadron in cp.squadrons:
|
for squadron in cp.squadrons:
|
||||||
unit_types.add(squadron.aircraft)
|
unit_types.add(squadron.aircraft)
|
||||||
|
|
||||||
sorted_units = sorted(unit_types, key=lambda u: u.name)
|
sorted_squadrons = sorted(cp.squadrons, key=lambda s: (s.aircraft.name, s.name))
|
||||||
for row, unit_type in enumerate(sorted_units):
|
for row, squadron in enumerate(sorted_squadrons):
|
||||||
self.add_purchase_row(unit_type, task_box_layout, row)
|
self.add_purchase_row(squadron, task_box_layout, row)
|
||||||
stretch = QVBoxLayout()
|
stretch = QVBoxLayout()
|
||||||
stretch.addStretch()
|
stretch.addStretch()
|
||||||
task_box_layout.addLayout(stretch, row, 0)
|
task_box_layout.addLayout(stretch, row, 0)
|
||||||
@ -65,76 +65,19 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
|||||||
main_layout.addWidget(scroll)
|
main_layout.addWidget(scroll)
|
||||||
self.setLayout(main_layout)
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
def enable_purchase(self, unit_type: AircraftType) -> bool:
|
|
||||||
if not super().enable_purchase(unit_type):
|
|
||||||
return False
|
|
||||||
if not self.cp.can_operate(unit_type):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def enable_sale(self, unit_type: AircraftType) -> bool:
|
|
||||||
return self.can_be_sold(unit_type)
|
|
||||||
|
|
||||||
def sell_tooltip(self, is_enabled: bool) -> str:
|
def sell_tooltip(self, is_enabled: bool) -> str:
|
||||||
if is_enabled:
|
if is_enabled:
|
||||||
return "Sell unit. Use Shift or Ctrl key to sell multiple units at once."
|
return "Sell unit. Use Shift or Ctrl key to sell multiple units at once."
|
||||||
else:
|
else:
|
||||||
return "Can not be sold because either no aircraft are available or are already assigned to a mission."
|
return (
|
||||||
|
"Can not be sold because either no aircraft are available or are "
|
||||||
def buy(self, unit_type: AircraftType) -> bool:
|
"already assigned to a mission."
|
||||||
if self.maximum_units > 0:
|
|
||||||
if self.cp.unclaimed_parking(self.game_model.game) <= 0:
|
|
||||||
logging.debug(f"No space for additional aircraft at {self.cp}.")
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
"No space for additional aircraft",
|
|
||||||
f"There is no parking space left at {self.cp.name} to accommodate "
|
|
||||||
"another plane.",
|
|
||||||
QMessageBox.Ok,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
# If we change our mind about selling, we want the aircraft to be put
|
|
||||||
# back in the inventory immediately.
|
|
||||||
elif self.pending_deliveries.units.get(unit_type, 0) < 0:
|
|
||||||
global_inventory = self.game_model.game.aircraft_inventory
|
|
||||||
inventory = global_inventory.for_control_point(self.cp)
|
|
||||||
inventory.add_aircraft(unit_type, 1)
|
|
||||||
|
|
||||||
super().buy(unit_type)
|
|
||||||
self.hangar_status.update_label()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def can_be_sold(self, unit_type: AircraftType) -> bool:
|
|
||||||
inventory = self.game_model.game.aircraft_inventory.for_control_point(self.cp)
|
|
||||||
pending_deliveries = self.pending_deliveries.units.get(unit_type, 0)
|
|
||||||
return self.cp.can_operate(unit_type) and (
|
|
||||||
pending_deliveries > 0 or inventory.available(unit_type) > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
def sell(self, unit_type: AircraftType) -> bool:
|
|
||||||
# Don't need to remove aircraft from the inventory if we're canceling
|
|
||||||
# orders.
|
|
||||||
if not self.can_be_sold(unit_type):
|
|
||||||
QMessageBox.critical(
|
|
||||||
self,
|
|
||||||
"Could not sell aircraft",
|
|
||||||
f"Attempted to sell one {unit_type} at {self.cp.name} "
|
|
||||||
"but none are available. Are all aircraft currently "
|
|
||||||
"assigned to a mission?",
|
|
||||||
QMessageBox.Ok,
|
|
||||||
)
|
)
|
||||||
return False
|
|
||||||
|
|
||||||
inventory = self.game_model.game.aircraft_inventory.for_control_point(self.cp)
|
def post_transaction_update(self) -> None:
|
||||||
pending_deliveries = self.pending_deliveries.units.get(unit_type, 0)
|
super().post_transaction_update()
|
||||||
if pending_deliveries <= 0 < inventory.available(unit_type):
|
|
||||||
inventory.remove_aircraft(unit_type, 1)
|
|
||||||
|
|
||||||
super().sell(unit_type)
|
|
||||||
self.hangar_status.update_label()
|
self.hangar_status.update_label()
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class QHangarStatus(QHBoxLayout):
|
class QHangarStatus(QHBoxLayout):
|
||||||
def __init__(self, game_model: GameModel, control_point: ControlPoint) -> None:
|
def __init__(self, game_model: GameModel, control_point: ControlPoint) -> None:
|
||||||
|
|||||||
@ -1,30 +1,27 @@
|
|||||||
from PySide2.QtCore import Qt
|
from PySide2.QtCore import Qt
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import QGridLayout, QScrollArea, QVBoxLayout, QWidget
|
||||||
QFrame,
|
|
||||||
QGridLayout,
|
|
||||||
QScrollArea,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
from qt_ui.models import GameModel
|
from qt_ui.models import GameModel
|
||||||
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
|
from qt_ui.windows.basemenu.UnitTransactionFrame import UnitTransactionFrame
|
||||||
|
from game.purchaseadapter import GroundUnitPurchaseAdapter
|
||||||
|
|
||||||
|
|
||||||
class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
|
class QArmorRecruitmentMenu(UnitTransactionFrame[GroundUnitType]):
|
||||||
def __init__(self, cp: ControlPoint, game_model: GameModel):
|
def __init__(self, cp: ControlPoint, game_model: GameModel):
|
||||||
QFrame.__init__(self)
|
super().__init__(
|
||||||
|
game_model,
|
||||||
|
GroundUnitPurchaseAdapter(
|
||||||
|
cp, game_model.game.coalition_for(cp.captured), game_model.game
|
||||||
|
),
|
||||||
|
)
|
||||||
self.cp = cp
|
self.cp = cp
|
||||||
self.game_model = game_model
|
self.game_model = game_model
|
||||||
self.purchase_groups = {}
|
self.purchase_groups = {}
|
||||||
self.bought_amount_labels = {}
|
self.bought_amount_labels = {}
|
||||||
self.existing_units_labels = {}
|
self.existing_units_labels = {}
|
||||||
|
|
||||||
self.init_ui()
|
|
||||||
|
|
||||||
def init_ui(self):
|
|
||||||
main_layout = QVBoxLayout()
|
main_layout = QVBoxLayout()
|
||||||
|
|
||||||
scroll_content = QWidget()
|
scroll_content = QWidget()
|
||||||
@ -50,11 +47,3 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
|
|||||||
scroll.setWidget(scroll_content)
|
scroll.setWidget(scroll_content)
|
||||||
main_layout.addWidget(scroll)
|
main_layout.addWidget(scroll)
|
||||||
self.setLayout(main_layout)
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
def enable_purchase(self, unit_type: GroundUnitType) -> bool:
|
|
||||||
if not super().enable_purchase(unit_type):
|
|
||||||
return False
|
|
||||||
return self.cp.has_ground_unit_source(self.game_model.game)
|
|
||||||
|
|
||||||
def enable_sale(self, unit_type: GroundUnitType) -> bool:
|
|
||||||
return self.pending_deliveries.pending_orders(unit_type) > 0
|
|
||||||
|
|||||||
@ -26,7 +26,7 @@ class QIntelInfo(QFrame):
|
|||||||
intel_layout = QVBoxLayout()
|
intel_layout = QVBoxLayout()
|
||||||
|
|
||||||
units_by_task: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
units_by_task: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
||||||
for unit_type, count in self.cp.base.aircraft.items():
|
for unit_type, count in self.cp.allocated_aircraft(game).present.items():
|
||||||
if count:
|
if count:
|
||||||
task_type = unit_type.dcs_unit_type.task_default.name
|
task_type = unit_type.dcs_unit_type.task_default.name
|
||||||
units_by_task[task_type][unit_type.name] += count
|
units_by_task[task_type][unit_type.name] += count
|
||||||
|
|||||||
@ -77,14 +77,15 @@ class AircraftIntelLayout(IntelTableLayout):
|
|||||||
|
|
||||||
total = 0
|
total = 0
|
||||||
for control_point in game.theater.control_points_for(player):
|
for control_point in game.theater.control_points_for(player):
|
||||||
base = control_point.base
|
allocation = control_point.allocated_aircraft(game)
|
||||||
total += base.total_aircraft
|
base_total = allocation.total_present
|
||||||
if not base.total_aircraft:
|
total += base_total
|
||||||
|
if not base_total:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.add_header(f"{control_point.name} ({base.total_aircraft})")
|
self.add_header(f"{control_point.name} ({base_total})")
|
||||||
for airframe in sorted(base.aircraft, key=lambda k: k.name):
|
for airframe in sorted(allocation.present, key=lambda k: k.name):
|
||||||
count = base.aircraft[airframe]
|
count = allocation.present[airframe]
|
||||||
if not count:
|
if not count:
|
||||||
continue
|
continue
|
||||||
self.add_row(f" {airframe.name}", count)
|
self.add_row(f" {airframe.name}", count)
|
||||||
|
|||||||
@ -177,7 +177,6 @@ class QPackageDialog(QDialog):
|
|||||||
|
|
||||||
def add_flight(self, flight: Flight) -> None:
|
def add_flight(self, flight: Flight) -> None:
|
||||||
"""Adds the new flight to the package."""
|
"""Adds the new flight to the package."""
|
||||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
|
||||||
self.package_model.add_flight(flight)
|
self.package_model.add_flight(flight)
|
||||||
planner = FlightPlanBuilder(
|
planner = FlightPlanBuilder(
|
||||||
self.package_model.package, self.game.blue, self.game.theater
|
self.package_model.package, self.game.blue, self.game.theater
|
||||||
@ -251,8 +250,7 @@ class QNewPackageDialog(QPackageDialog):
|
|||||||
def on_cancel(self) -> None:
|
def on_cancel(self) -> None:
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
for flight in self.package_model.package.flights:
|
for flight in self.package_model.package.flights:
|
||||||
self.game.aircraft_inventory.return_from_flight(flight)
|
flight.return_pilots_and_aircraft()
|
||||||
flight.clear_roster()
|
|
||||||
|
|
||||||
|
|
||||||
class QEditPackageDialog(QPackageDialog):
|
class QEditPackageDialog(QPackageDialog):
|
||||||
|
|||||||
@ -24,7 +24,6 @@ from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
|||||||
from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector
|
from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector
|
||||||
from qt_ui.widgets.combos.QArrivalAirfieldSelector import QArrivalAirfieldSelector
|
from qt_ui.widgets.combos.QArrivalAirfieldSelector import QArrivalAirfieldSelector
|
||||||
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
|
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
|
||||||
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
|
|
||||||
from qt_ui.windows.mission.flight.SquadronSelector import SquadronSelector
|
from qt_ui.windows.mission.flight.SquadronSelector import SquadronSelector
|
||||||
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import FlightRosterEditor
|
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import FlightRosterEditor
|
||||||
|
|
||||||
@ -34,6 +33,7 @@ class QFlightCreator(QDialog):
|
|||||||
|
|
||||||
def __init__(self, game: Game, package: Package, parent=None) -> None:
|
def __init__(self, game: Game, package: Package, parent=None) -> None:
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
self.setMinimumWidth(400)
|
||||||
|
|
||||||
self.game = game
|
self.game = game
|
||||||
self.package = package
|
self.package = package
|
||||||
@ -51,7 +51,7 @@ class QFlightCreator(QDialog):
|
|||||||
layout.addLayout(QLabeledWidget("Task:", self.task_selector))
|
layout.addLayout(QLabeledWidget("Task:", self.task_selector))
|
||||||
|
|
||||||
self.aircraft_selector = QAircraftTypeSelector(
|
self.aircraft_selector = QAircraftTypeSelector(
|
||||||
self.game.aircraft_inventory.available_types_for_player,
|
self.game.blue.air_wing.available_aircraft_types,
|
||||||
self.task_selector.currentData(),
|
self.task_selector.currentData(),
|
||||||
)
|
)
|
||||||
self.aircraft_selector.setCurrentIndex(0)
|
self.aircraft_selector.setCurrentIndex(0)
|
||||||
@ -66,22 +66,6 @@ class QFlightCreator(QDialog):
|
|||||||
self.squadron_selector.setCurrentIndex(0)
|
self.squadron_selector.setCurrentIndex(0)
|
||||||
layout.addLayout(QLabeledWidget("Squadron:", self.squadron_selector))
|
layout.addLayout(QLabeledWidget("Squadron:", self.squadron_selector))
|
||||||
|
|
||||||
self.departure = QOriginAirfieldSelector(
|
|
||||||
self.game.aircraft_inventory,
|
|
||||||
[cp for cp in game.theater.controlpoints if cp.captured],
|
|
||||||
self.aircraft_selector.currentData(),
|
|
||||||
)
|
|
||||||
self.departure.availability_changed.connect(self.update_max_size)
|
|
||||||
self.departure.currentIndexChanged.connect(self.on_departure_changed)
|
|
||||||
layout.addLayout(QLabeledWidget("Departure:", self.departure))
|
|
||||||
|
|
||||||
self.arrival = QArrivalAirfieldSelector(
|
|
||||||
[cp for cp in game.theater.controlpoints if cp.captured],
|
|
||||||
self.aircraft_selector.currentData(),
|
|
||||||
"Same as departure",
|
|
||||||
)
|
|
||||||
layout.addLayout(QLabeledWidget("Arrival:", self.arrival))
|
|
||||||
|
|
||||||
self.divert = QArrivalAirfieldSelector(
|
self.divert = QArrivalAirfieldSelector(
|
||||||
[cp for cp in game.theater.controlpoints if cp.captured],
|
[cp for cp in game.theater.controlpoints if cp.captured],
|
||||||
self.aircraft_selector.currentData(),
|
self.aircraft_selector.currentData(),
|
||||||
@ -90,7 +74,7 @@ class QFlightCreator(QDialog):
|
|||||||
layout.addLayout(QLabeledWidget("Divert:", self.divert))
|
layout.addLayout(QLabeledWidget("Divert:", self.divert))
|
||||||
|
|
||||||
self.flight_size_spinner = QFlightSizeSpinner()
|
self.flight_size_spinner = QFlightSizeSpinner()
|
||||||
self.update_max_size(self.departure.available)
|
self.update_max_size(self.squadron_selector.aircraft_available)
|
||||||
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
|
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
|
||||||
|
|
||||||
squadron = self.squadron_selector.currentData()
|
squadron = self.squadron_selector.currentData()
|
||||||
@ -144,8 +128,6 @@ class QFlightCreator(QDialog):
|
|||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
self.on_departure_changed(self.departure.currentIndex())
|
|
||||||
|
|
||||||
def reject(self) -> None:
|
def reject(self) -> None:
|
||||||
super().reject()
|
super().reject()
|
||||||
# Clear the roster to return pilots to the pool.
|
# Clear the roster to return pilots to the pool.
|
||||||
@ -161,25 +143,19 @@ class QFlightCreator(QDialog):
|
|||||||
def verify_form(self) -> Optional[str]:
|
def verify_form(self) -> Optional[str]:
|
||||||
aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData()
|
aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData()
|
||||||
squadron: Optional[Squadron] = self.squadron_selector.currentData()
|
squadron: Optional[Squadron] = self.squadron_selector.currentData()
|
||||||
origin: Optional[ControlPoint] = self.departure.currentData()
|
|
||||||
arrival: Optional[ControlPoint] = self.arrival.currentData()
|
|
||||||
divert: Optional[ControlPoint] = self.divert.currentData()
|
divert: Optional[ControlPoint] = self.divert.currentData()
|
||||||
size: int = self.flight_size_spinner.value()
|
size: int = self.flight_size_spinner.value()
|
||||||
if aircraft is None:
|
if aircraft is None:
|
||||||
return "You must select an aircraft type."
|
return "You must select an aircraft type."
|
||||||
if squadron is None:
|
if squadron is None:
|
||||||
return "You must select a squadron."
|
return "You must select a squadron."
|
||||||
if not origin.captured:
|
|
||||||
return f"{origin.name} is not owned by your coalition."
|
|
||||||
if arrival is not None and not arrival.captured:
|
|
||||||
return f"{arrival.name} is not owned by your coalition."
|
|
||||||
if divert is not None and not divert.captured:
|
if divert is not None and not divert.captured:
|
||||||
return f"{divert.name} is not owned by your coalition."
|
return f"{divert.name} is not owned by your coalition."
|
||||||
available = origin.base.aircraft.get(aircraft, 0)
|
available = squadron.untasked_aircraft
|
||||||
if not available:
|
if not available:
|
||||||
return f"{origin.name} has no {aircraft.id} available."
|
return f"{squadron} has no aircraft available."
|
||||||
if size > available:
|
if size > available:
|
||||||
return f"{origin.name} has only {available} {aircraft.id} available."
|
return f"{squadron} has only {available} aircraft available."
|
||||||
if size <= 0:
|
if size <= 0:
|
||||||
return f"Flight must have at least one aircraft."
|
return f"Flight must have at least one aircraft."
|
||||||
if self.custom_name_text and "|" in self.custom_name_text:
|
if self.custom_name_text and "|" in self.custom_name_text:
|
||||||
@ -194,14 +170,9 @@ class QFlightCreator(QDialog):
|
|||||||
|
|
||||||
task = self.task_selector.currentData()
|
task = self.task_selector.currentData()
|
||||||
squadron = self.squadron_selector.currentData()
|
squadron = self.squadron_selector.currentData()
|
||||||
origin = self.departure.currentData()
|
|
||||||
arrival = self.arrival.currentData()
|
|
||||||
divert = self.divert.currentData()
|
divert = self.divert.currentData()
|
||||||
roster = self.roster_editor.roster
|
roster = self.roster_editor.roster
|
||||||
|
|
||||||
if arrival is None:
|
|
||||||
arrival = origin
|
|
||||||
|
|
||||||
flight = Flight(
|
flight = Flight(
|
||||||
self.package,
|
self.package,
|
||||||
self.country,
|
self.country,
|
||||||
@ -211,8 +182,8 @@ class QFlightCreator(QDialog):
|
|||||||
roster.max_size,
|
roster.max_size,
|
||||||
task,
|
task,
|
||||||
self.start_type.currentText(),
|
self.start_type.currentText(),
|
||||||
origin,
|
squadron.location,
|
||||||
arrival,
|
squadron.location,
|
||||||
divert,
|
divert,
|
||||||
custom_name=self.custom_name_text,
|
custom_name=self.custom_name_text,
|
||||||
roster=roster,
|
roster=roster,
|
||||||
@ -228,11 +199,9 @@ class QFlightCreator(QDialog):
|
|||||||
self.task_selector.currentData(), new_aircraft
|
self.task_selector.currentData(), new_aircraft
|
||||||
)
|
)
|
||||||
self.departure.change_aircraft(new_aircraft)
|
self.departure.change_aircraft(new_aircraft)
|
||||||
self.arrival.change_aircraft(new_aircraft)
|
|
||||||
self.divert.change_aircraft(new_aircraft)
|
self.divert.change_aircraft(new_aircraft)
|
||||||
|
|
||||||
def on_departure_changed(self, index: int) -> None:
|
def on_departure_changed(self, departure: ControlPoint) -> None:
|
||||||
departure = self.departure.itemData(index)
|
|
||||||
if isinstance(departure, OffMapSpawn):
|
if isinstance(departure, OffMapSpawn):
|
||||||
previous_type = self.start_type.currentText()
|
previous_type = self.start_type.currentText()
|
||||||
if previous_type != "In Flight":
|
if previous_type != "In Flight":
|
||||||
@ -248,12 +217,12 @@ class QFlightCreator(QDialog):
|
|||||||
def on_task_changed(self, index: int) -> None:
|
def on_task_changed(self, index: int) -> None:
|
||||||
task = self.task_selector.itemData(index)
|
task = self.task_selector.itemData(index)
|
||||||
self.aircraft_selector.update_items(
|
self.aircraft_selector.update_items(
|
||||||
task, self.game.aircraft_inventory.available_types_for_player
|
task, self.game.blue.air_wing.available_aircraft_types
|
||||||
)
|
)
|
||||||
self.squadron_selector.update_items(task, self.aircraft_selector.currentData())
|
self.squadron_selector.update_items(task, self.aircraft_selector.currentData())
|
||||||
|
|
||||||
def on_squadron_changed(self, index: int) -> None:
|
def on_squadron_changed(self, index: int) -> None:
|
||||||
squadron = self.squadron_selector.itemData(index)
|
squadron: Optional[Squadron] = self.squadron_selector.itemData(index)
|
||||||
# Clear the roster first so we return the pilots to the pool. This way if we end
|
# Clear the roster first so we return the pilots to the pool. This way if we end
|
||||||
# up repopulating from the same squadron we'll get the same pilots back.
|
# up repopulating from the same squadron we'll get the same pilots back.
|
||||||
self.roster_editor.replace(None)
|
self.roster_editor.replace(None)
|
||||||
@ -261,6 +230,7 @@ class QFlightCreator(QDialog):
|
|||||||
self.roster_editor.replace(
|
self.roster_editor.replace(
|
||||||
FlightRoster(squadron, self.flight_size_spinner.value())
|
FlightRoster(squadron, self.flight_size_spinner.value())
|
||||||
)
|
)
|
||||||
|
self.on_departure_changed(squadron.location)
|
||||||
|
|
||||||
def update_max_size(self, available: int) -> None:
|
def update_max_size(self, available: int) -> None:
|
||||||
aircraft = self.aircraft_selector.currentData()
|
aircraft = self.aircraft_selector.currentData()
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
"""Combo box for selecting squadrons."""
|
"""Combo box for selecting squadrons."""
|
||||||
from typing import Type, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PySide2.QtWidgets import QComboBox
|
from PySide2.QtWidgets import QComboBox
|
||||||
from dcs.unittype import FlyingType
|
|
||||||
|
|
||||||
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.squadrons.airwing import AirWing
|
from game.squadrons.airwing import AirWing
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ class SquadronSelector(QComboBox):
|
|||||||
self,
|
self,
|
||||||
air_wing: AirWing,
|
air_wing: AirWing,
|
||||||
task: Optional[FlightType],
|
task: Optional[FlightType],
|
||||||
aircraft: Optional[Type[FlyingType]],
|
aircraft: Optional[AircraftType],
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.air_wing = air_wing
|
self.air_wing = air_wing
|
||||||
@ -24,8 +24,15 @@ class SquadronSelector(QComboBox):
|
|||||||
self.setSizeAdjustPolicy(self.AdjustToContents)
|
self.setSizeAdjustPolicy(self.AdjustToContents)
|
||||||
self.update_items(task, aircraft)
|
self.update_items(task, aircraft)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def aircraft_available(self) -> int:
|
||||||
|
squadron = self.currentData()
|
||||||
|
if squadron is None:
|
||||||
|
return 0
|
||||||
|
return squadron.untasked_aircraft
|
||||||
|
|
||||||
def update_items(
|
def update_items(
|
||||||
self, task: Optional[FlightType], aircraft: Optional[Type[FlyingType]]
|
self, task: Optional[FlightType], aircraft: Optional[AircraftType]
|
||||||
) -> None:
|
) -> None:
|
||||||
current_squadron = self.currentData()
|
current_squadron = self.currentData()
|
||||||
self.blockSignals(True)
|
self.blockSignals(True)
|
||||||
@ -41,12 +48,12 @@ class SquadronSelector(QComboBox):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for squadron in self.air_wing.squadrons_for(aircraft):
|
for squadron in self.air_wing.squadrons_for(aircraft):
|
||||||
if task in squadron.mission_types:
|
if task in squadron.mission_types and squadron.untasked_aircraft:
|
||||||
self.addItem(f"{squadron}", squadron)
|
self.addItem(f"{squadron.location}: {squadron}", squadron)
|
||||||
|
|
||||||
if self.count() == 0:
|
if self.count() == 0:
|
||||||
self.addItem("No capable aircraft available", None)
|
self.addItem("No capable aircraft available", None)
|
||||||
return
|
return
|
||||||
|
|
||||||
if current_squadron is not None:
|
if current_squadron is not None:
|
||||||
self.setCurrentText(f"{current_squadron}")
|
self.setCurrentText(f"{current_squadron.location}: {current_squadron}")
|
||||||
|
|||||||
@ -195,8 +195,7 @@ class QFlightSlotEditor(QGroupBox):
|
|||||||
self.package_model = package_model
|
self.package_model = package_model
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.game = game
|
self.game = game
|
||||||
self.inventory = self.game.aircraft_inventory.for_control_point(flight.from_cp)
|
available = self.flight.squadron.untasked_aircraft
|
||||||
available = self.inventory.available(self.flight.unit_type)
|
|
||||||
max_count = self.flight.count + available
|
max_count = self.flight.count + available
|
||||||
if max_count > 4:
|
if max_count > 4:
|
||||||
max_count = 4
|
max_count = 4
|
||||||
@ -225,21 +224,18 @@ class QFlightSlotEditor(QGroupBox):
|
|||||||
def _changed_aircraft_count(self):
|
def _changed_aircraft_count(self):
|
||||||
old_count = self.flight.count
|
old_count = self.flight.count
|
||||||
new_count = int(self.aircraft_count_spinner.value())
|
new_count = int(self.aircraft_count_spinner.value())
|
||||||
self.game.aircraft_inventory.return_from_flight(self.flight)
|
|
||||||
self.flight.resize(new_count)
|
|
||||||
try:
|
try:
|
||||||
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
self.flight.resize(new_count)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# The UI should have prevented this, but if we ran out of aircraft
|
# The UI should have prevented this, but if we ran out of aircraft
|
||||||
# then roll back the inventory change.
|
# then roll back the inventory change.
|
||||||
difference = new_count - self.flight.count
|
difference = new_count - self.flight.count
|
||||||
available = self.inventory.available(self.flight.unit_type)
|
available = self.flight.squadron.untasked_aircraft
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Could not add {difference} additional aircraft to "
|
f"Could not add {difference} additional aircraft to "
|
||||||
f"{self.flight} because {self.flight.departure} has only "
|
f"{self.flight} because {self.flight.departure} has only "
|
||||||
f"{available} {self.flight.unit_type} remaining"
|
f"{available} {self.flight.unit_type} remaining"
|
||||||
)
|
)
|
||||||
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
|
||||||
self.flight.resize(old_count)
|
self.flight.resize(old_count)
|
||||||
return
|
return
|
||||||
self.roster_editor.resize(new_count)
|
self.roster_editor.resize(new_count)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user