From 31c80dfd0231ff67afd6967f2c1ec3b3b31f4fc8 Mon Sep 17 00:00:00 2001 From: Eclipse/Druss99 Date: Fri, 17 Jan 2025 17:02:07 -0500 Subject: [PATCH] refactor of previous commits refactor to enum typing and many other fixes fix tests attempt to fix some typescript more typescript fixes more typescript test fixes revert all API changes update to pydcs mypy fixes Use properties to check if player is blue/red/neutral update requirements.txt black -_- bump pydcs and fix mypy add opponent property bump pydcs --- game/armedforces/forcegroup.py | 5 + game/ato/flight.py | 3 +- game/ato/flightplans/ibuilder.py | 4 +- game/campaignloader/campaignairwingconfig.py | 1 - game/campaignloader/mizcampaignloader.py | 44 +++--- game/coalition.py | 18 ++- game/commander/objectivefinder.py | 13 +- game/commander/packagefulfiller.py | 4 +- game/commander/tasks/compound/capturebase.py | 6 +- game/commander/tasks/frontlinestancetask.py | 3 +- game/commander/tasks/primitive/cas.py | 7 +- game/commander/theatercommander.py | 3 +- game/commander/theaterstate.py | 7 +- game/debriefing.py | 62 ++++---- game/game.py | 46 +++--- game/groundunitorders.py | 2 +- game/income.py | 5 +- .../aircraft/aircraftgenerator.py | 8 +- game/missiongenerator/aircraft/flightdata.py | 3 +- .../aircraft/flightgroupconfigurator.py | 2 +- game/missiongenerator/briefinggenerator.py | 4 +- game/missiongenerator/convoygenerator.py | 3 +- game/missiongenerator/drawingsgenerator.py | 11 +- game/missiongenerator/flotgenerator.py | 22 +-- game/missiongenerator/luagenerator.py | 7 +- game/missiongenerator/missiondata.py | 5 +- game/missiongenerator/missiongenerator.py | 9 +- game/missiongenerator/rebelliongenerator.py | 5 +- game/missiongenerator/tgogenerator.py | 31 +++- game/missiongenerator/triggergenerator.py | 59 ++++++- game/models/game_stats.py | 2 +- game/pretense/pretenseaircraftgenerator.py | 5 +- game/pretense/pretenseflightgroupspawner.py | 4 +- game/pretense/pretenseluagenerator.py | 8 +- game/pretense/pretensemissiongenerator.py | 3 +- game/pretense/pretensetgogenerator.py | 7 +- game/pretense/pretensetriggergenerator.py | 22 +-- game/procurement.py | 14 +- game/server/controlpoints/models.py | 8 +- game/server/controlpoints/routes.py | 5 +- game/server/eventstream/models.py | 5 +- game/server/flights/models.py | 6 +- game/server/iadsnetwork/models.py | 13 +- game/server/mapzones/models.py | 6 +- game/server/navmesh/routes.py | 3 +- game/server/supplyroutes/models.py | 6 +- game/server/tgos/models.py | 6 +- game/sim/combat/aircombat.py | 4 +- game/sim/combat/combatinitiator.py | 9 +- game/sim/combat/samengagementzones.py | 6 +- game/sim/gameupdateevents.py | 12 +- game/sim/missionresultsprocessor.py | 2 +- game/squadrons/airwing.py | 3 +- game/squadrons/squadron.py | 6 +- game/theater/conflicttheater.py | 22 ++- game/theater/controlpoint.py | 71 +++++---- game/theater/frontline.py | 18 ++- game/theater/iadsnetwork/iadsnetwork.py | 9 +- game/theater/missiontarget.py | 6 +- game/theater/player.py | 30 ++++ game/theater/start_generator.py | 25 +-- game/theater/theatergroundobject.py | 32 ++-- game/theater/transitnetwork.py | 3 +- game/threatzones.py | 3 +- game/transfers.py | 33 ++-- qt_ui/models.py | 6 +- qt_ui/widgets/QIntelBox.py | 5 +- .../QPredefinedWaypointSelectionComboBox.py | 3 +- qt_ui/windows/AirWingConfigurationDialog.py | 2 +- qt_ui/windows/QDebriefingWindow.py | 17 +- .../windows/basemenu/NewUnitTransferDialog.py | 4 +- qt_ui/windows/basemenu/QBaseMenu2.py | 3 +- .../windows/groundobject/QGroundObjectMenu.py | 12 +- requirements.txt | 4 + resources/campaigns/scenic_rout_neutral.yaml | 147 ++++++++++++++++++ resources/campaigns/scenic_route_neutral.miz | Bin 0 -> 113557 bytes tests/theater/test_controlpoint.py | 43 ++--- tests/theater/test_theatergroundobject.py | 24 +-- 78 files changed, 739 insertions(+), 350 deletions(-) create mode 100644 game/theater/player.py create mode 100644 resources/campaigns/scenic_rout_neutral.yaml create mode 100644 resources/campaigns/scenic_route_neutral.miz diff --git a/game/armedforces/forcegroup.py b/game/armedforces/forcegroup.py index af62c6e7..e80f1272 100644 --- a/game/armedforces/forcegroup.py +++ b/game/armedforces/forcegroup.py @@ -264,6 +264,11 @@ class ForceGroup: units = unit_group.generate_units( ground_object, unit_type, unit_count, fixed_pos, fixed_hdg ) + # If the control point is neutral, the units are dead + if ground_object.control_point.captured.is_neutral: + for unit in units: + if not unit.is_static: + unit.alive = False # Get or create the TheaterGroup ground_group = ground_object.group_by_name(group_name) if ground_group is not None: diff --git a/game/ato/flight.py b/game/ato/flight.py index 1cfe8040..743d7518 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -35,6 +35,7 @@ if TYPE_CHECKING: from game.sim.gameupdateevents import GameUpdateEvents from game.sim.simulationresults import SimulationResults from game.squadrons import Squadron, Pilot + from game.theater.player import Player from game.transfers import TransferOrder from game.data.weapons import WeaponType from .flightmember import FlightMember @@ -174,7 +175,7 @@ class Flight( self.roster = FlightMembers.from_roster(self, self.roster) @property - def blue(self) -> bool: + def blue(self) -> Player: return self.squadron.player @property diff --git a/game/ato/flightplans/ibuilder.py b/game/ato/flightplans/ibuilder.py index 31459f39..d7a3b6f1 100644 --- a/game/ato/flightplans/ibuilder.py +++ b/game/ato/flightplans/ibuilder.py @@ -11,7 +11,7 @@ from ..packagewaypoints import PackageWaypoints if TYPE_CHECKING: from game.coalition import Coalition from game.data.doctrine import Doctrine - from game.theater import ConflictTheater + from game.theater import ConflictTheater, Player from game.threatzones import ThreatZones from ..flight import Flight from ..package import Package @@ -71,7 +71,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]): return self.flight.coalition @property - def is_player(self) -> bool: + def is_player(self) -> Player: return self.coalition.player @property diff --git a/game/campaignloader/campaignairwingconfig.py b/game/campaignloader/campaignairwingconfig.py index af3ded5d..b0f75388 100644 --- a/game/campaignloader/campaignairwingconfig.py +++ b/game/campaignloader/campaignairwingconfig.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging - from collections import defaultdict from dataclasses import dataclass from typing import Any, Optional, TYPE_CHECKING, Union diff --git a/game/campaignloader/mizcampaignloader.py b/game/campaignloader/mizcampaignloader.py index 7584ad3d..d72a24f3 100644 --- a/game/campaignloader/mizcampaignloader.py +++ b/game/campaignloader/mizcampaignloader.py @@ -26,6 +26,7 @@ from game.theater.controlpoint import ( Carrier, ControlPoint, ControlPointType, + Player, Fob, Lha, OffMapSpawn, @@ -123,9 +124,13 @@ class MizCampaignLoader: def control_point_from_airport( self, airport: Airport, ctld_zones: List[Tuple[Point, float]] ) -> ControlPoint: - cp = Airfield( - airport, self.theater, starts_blue=airport.is_blue(), ctld_zones=ctld_zones - ) + if airport.dynamic_spawn: + starting_coalition = Player.NEUTRAL + elif airport.is_blue(): + starting_coalition = Player.BLUE + else: + starting_coalition = Player.RED + cp = Airfield(airport, self.theater, starting_coalition, ctld_zones=ctld_zones) # Use the unlimited aircraft option to determine if an airfield should # be owned by the player when the campaign is "inverted". @@ -133,43 +138,44 @@ class MizCampaignLoader: return cp - def country(self, blue: bool) -> Country: - country = self.mission.country( - self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name - ) + def country(self, blue: Player) -> Country: + if blue.is_blue: + country = self.mission.country(self.BLUE_COUNTRY.name) + else: + country = self.mission.country(self.RED_COUNTRY.name) # Should be guaranteed because we initialized them. assert country return country @property def blue(self) -> Country: - return self.country(blue=True) + return self.country(blue=Player.BLUE) @property def red(self) -> Country: - return self.country(blue=False) + return self.country(blue=Player.RED) - def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]: + def off_map_spawns(self, blue: Player) -> Iterator[PlaneGroup]: for group in self.country(blue).plane_group: if group.units[0].type == self.OFF_MAP_UNIT_TYPE: yield group - def carriers(self, blue: bool) -> Iterator[ShipGroup]: + def carriers(self, blue: Player) -> Iterator[ShipGroup]: for group in self.country(blue).ship_group: if group.units[0].type == self.CV_UNIT_TYPE: yield group - def lhas(self, blue: bool) -> Iterator[ShipGroup]: + def lhas(self, blue: Player) -> Iterator[ShipGroup]: for group in self.country(blue).ship_group: if group.units[0].type == self.LHA_UNIT_TYPE: yield group - def fobs(self, blue: bool) -> Iterator[VehicleGroup]: + def fobs(self, blue: Player) -> Iterator[VehicleGroup]: for group in self.country(blue).vehicle_group: if group.units[0].type == self.FOB_UNIT_TYPE: yield group - def invisible_fobs(self, blue: bool) -> Iterator[VehicleGroup]: + def invisible_fobs(self, blue: Player) -> Iterator[VehicleGroup]: for group in self.country(blue).vehicle_group: if group.units[0].type == self.INVISIBLE_FOB_UNIT_TYPE: yield group @@ -290,12 +296,12 @@ class MizCampaignLoader: def control_points(self) -> dict[UUID, ControlPoint]: control_points = {} for airport in self.mission.terrain.airport_list(): - if airport.is_blue() or airport.is_red(): + if airport.is_blue() or airport.is_red() or airport.is_neutral(): ctld_zones = self.get_ctld_zones(airport.name) control_point = self.control_point_from_airport(airport, ctld_zones) control_points[control_point.id] = control_point - for blue in (False, True): + for blue in (Player.RED, Player.BLUE): for group in self.off_map_spawns(blue): control_point = OffMapSpawn( str(group.name), group.position, self.theater, starts_blue=blue @@ -348,13 +354,13 @@ class MizCampaignLoader: @property def front_line_path_groups(self) -> Iterator[VehicleGroup]: - for group in self.country(blue=True).vehicle_group: + for group in self.country(blue=Player.BLUE).vehicle_group: if group.units[0].type == self.FRONT_LINE_UNIT_TYPE: yield group @property def shipping_lane_groups(self) -> Iterator[ShipGroup]: - for group in self.country(blue=True).ship_group: + for group in self.country(blue=Player.BLUE).ship_group: if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE: yield group @@ -378,7 +384,7 @@ class MizCampaignLoader: @property def cp_convoy_spawns(self) -> Iterator[VehicleGroup]: - for group in self.country(blue=True).vehicle_group: + for group in self.country(blue=Player.BLUE).vehicle_group: if group.units[0].type == self.CP_CONVOY_SPAWN_TYPE: yield group diff --git a/game/coalition.py b/game/coalition.py index bd1adfe6..c8fa9b2a 100644 --- a/game/coalition.py +++ b/game/coalition.py @@ -17,6 +17,7 @@ from game.procurement import AircraftProcurementRequest, ProcurementAi from game.profiling import MultiEventTracer, logged_duration from game.squadrons import AirWing from game.theater.bullseye import Bullseye +from game.theater.player import Player from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder from game.threatzones import ThreatZones from game.transfers import PendingTransfers @@ -32,7 +33,7 @@ if TYPE_CHECKING: class Coalition: def __init__( - self, game: Game, faction: Faction, budget: float, player: bool + self, game: Game, faction: Faction, budget: float, player: Player ) -> None: self.game = game self.player = player @@ -68,9 +69,11 @@ class Coalition: @property def coalition_id(self) -> int: - if self.player: + if self.player.is_blue: return 2 - return 1 + elif self.player.is_red: + return 1 + return 0 @property def opponent(self) -> Coalition: @@ -95,8 +98,9 @@ class Coalition: state = self.__dict__.copy() # Avoid persisting any volatile types that can be deterministically # recomputed on load for the sake of save compatibility. - del state["_threat_zone"] - del state["_navmesh"] + if state["player"] != Player.NEUTRAL: + del state["_threat_zone"] + del state["_navmesh"] del state["faker"] return state @@ -203,7 +207,7 @@ class Coalition: squadron.refund_orders() def plan_missions(self, now: datetime) -> None: - color = "Blue" if self.player else "Red" + color = "Blue" if self.player.is_blue else "Red" with MultiEventTracer() as tracer: with tracer.trace(f"{color} mission planning"): with tracer.trace(f"{color} mission identification"): @@ -220,7 +224,7 @@ class Coalition: # to ground forces and 1400 to aircraft. After that the budget will be spent # proportionally based on how much is already invested. - if self.player: + if self.player.is_blue: manage_runways = self.game.settings.automate_runway_repair manage_front_line = self.game.settings.automate_front_line_reinforcements manage_aircraft = self.game.settings.automate_aircraft_reinforcements diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index d00905da..674ffa75 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -16,6 +16,7 @@ from game.theater import ( OffMapSpawn, ParkingType, NavalControlPoint, + Player, ) from game.theater.theatergroundobject import ( BuildingGroundObject, @@ -35,7 +36,7 @@ MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget) class ObjectiveFinder: """Identifies potential objectives for the mission planner.""" - def __init__(self, game: Game, is_player: bool) -> None: + def __init__(self, game: Game, is_player: Player) -> None: self.game = game self.is_player = is_player @@ -154,7 +155,7 @@ class ObjectiveFinder: airfields_in_proximity = self.closest_airfields_to(cp) airbase_threat_range = self.game.settings.airbase_threat_range if ( - not self.is_player + self.is_player.is_red and randint(1, 100) > self.game.settings.opfor_autoplanner_aggressiveness ): @@ -216,7 +217,7 @@ class ObjectiveFinder: def farthest_friendly_control_point(self) -> ControlPoint: """Finds the friendly control point that is farthest from any threats.""" - threat_zones = self.game.threat_zone_for(not self.is_player) + threat_zones = self.game.threat_zone_for(self.is_player.opponent) farthest = None max_distance = meters(0) @@ -234,7 +235,7 @@ class ObjectiveFinder: def closest_friendly_control_point(self) -> ControlPoint: """Finds the friendly control point that is closest to any threats.""" - threat_zones = self.game.threat_zone_for(not self.is_player) + threat_zones = self.game.threat_zone_for(self.is_player.opponent) closest = None min_distance = meters(math.inf) @@ -258,14 +259,14 @@ class ObjectiveFinder: return ( c for c in self.game.theater.controlpoints - if not c.is_friendly(self.is_player) + if not c.is_friendly(self.is_player) and c.captured != Player.NEUTRAL ) def prioritized_points(self) -> list[ControlPoint]: prioritized = [] capturable_later = [] isolated = [] - for cp in self.game.theater.control_points_for(not self.is_player): + for cp in self.game.theater.control_points_for(self.is_player.opponent): if cp.is_isolated: isolated.append(cp) continue diff --git a/game/commander/packagefulfiller.py b/game/commander/packagefulfiller.py index 71ddef62..9a0a1975 100644 --- a/game/commander/packagefulfiller.py +++ b/game/commander/packagefulfiller.py @@ -43,7 +43,9 @@ class PackageFulfiller: @property def is_player(self) -> bool: - return self.coalition.player + if self.coalition.player.is_blue: + return True + return False @property def ato(self) -> AirTaskingOrder: diff --git a/game/commander/tasks/compound/capturebase.py b/game/commander/tasks/compound/capturebase.py index 378cb13e..7034fa2a 100644 --- a/game/commander/tasks/compound/capturebase.py +++ b/game/commander/tasks/compound/capturebase.py @@ -10,7 +10,7 @@ from game.commander.tasks.compound.reduceenemyfrontlinecapacity import ( from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack from game.commander.theaterstate import TheaterState from game.htn import CompoundTask, Method -from game.theater import ControlPoint, FrontLine +from game.theater import ControlPoint, FrontLine, Player @dataclass(frozen=True) @@ -26,12 +26,12 @@ class CaptureBase(CompoundTask[TheaterState]): def enemy_cp(self, state: TheaterState) -> ControlPoint: return self.front_line.control_point_hostile_to(state.context.coalition.player) - def units_deployable(self, state: TheaterState, player: bool) -> int: + def units_deployable(self, state: TheaterState, player: Player) -> int: cp = self.front_line.control_point_friendly_to(player) ammo_depots = list(state.ammo_dumps_at(cp)) return cp.deployable_front_line_units_with(len(ammo_depots)) - def unit_cap(self, state: TheaterState, player: bool) -> int: + def unit_cap(self, state: TheaterState, player: Player) -> int: cp = self.front_line.control_point_friendly_to(player) ammo_depots = list(state.ammo_dumps_at(cp)) return cp.front_line_capacity_with(len(ammo_depots)) diff --git a/game/commander/tasks/frontlinestancetask.py b/game/commander/tasks/frontlinestancetask.py index c6240262..64a2e790 100644 --- a/game/commander/tasks/frontlinestancetask.py +++ b/game/commander/tasks/frontlinestancetask.py @@ -11,10 +11,11 @@ from game.theater import FrontLine if TYPE_CHECKING: from game.coalition import Coalition + from game.theater.player import Player class FrontLineStanceTask(TheaterCommanderTask, ABC): - def __init__(self, front_line: FrontLine, player: bool) -> None: + def __init__(self, front_line: FrontLine, player: Player) -> None: self.front_line = front_line self.friendly_cp = self.front_line.control_point_friendly_to(player) self.enemy_cp = self.front_line.control_point_hostile_to(player) diff --git a/game/commander/tasks/primitive/cas.py b/game/commander/tasks/primitive/cas.py index c6fc47e5..2d1d3868 100644 --- a/game/commander/tasks/primitive/cas.py +++ b/game/commander/tasks/primitive/cas.py @@ -6,7 +6,7 @@ from game.ato.flighttype import FlightType from game.commander.missionproposals import EscortType from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.theater import FrontLine +from game.theater import FrontLine, Player @dataclass @@ -19,9 +19,8 @@ class PlanCas(PackagePlanningTask[FrontLine]): # An exception is made for turn zero since that's not being truly planned, but # just to determine what missions should be planned on turn 1 (when there *will* # be ground units) and what aircraft should be ordered. - enemy_cp = self.target.control_point_friendly_to( - player=not state.context.coalition.player - ) + player = state.context.coalition.player.opponent + enemy_cp = self.target.control_point_friendly_to(player) if enemy_cp.deployable_front_line_units == 0 and state.context.turn > 0: return False return super().preconditions_met(state) diff --git a/game/commander/theatercommander.py b/game/commander/theatercommander.py index 23f64a6f..8e8b91bf 100644 --- a/game/commander/theatercommander.py +++ b/game/commander/theatercommander.py @@ -67,10 +67,11 @@ from game.profiling import MultiEventTracer if TYPE_CHECKING: from game import Game + from game.theater.player import Player class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]): - def __init__(self, game: Game, player: bool) -> None: + def __init__(self, game: Game, player: Player) -> None: super().__init__( PlanNextAction( aircraft_cold_start=game.settings.default_start_type is StartType.COLD diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py index 7fd983db..4ee140f8 100644 --- a/game/commander/theaterstate.py +++ b/game/commander/theaterstate.py @@ -20,6 +20,7 @@ from game.theater import ( ControlPoint, FrontLine, MissionTarget, + Player, ) from game.theater.theatergroundobject import ( BuildingGroundObject, @@ -152,7 +153,7 @@ class TheaterState(WorldState["TheaterState"]): @classmethod def from_game( - cls, game: Game, player: bool, now: datetime, tracer: MultiEventTracer + cls, game: Game, player: Player, now: datetime, tracer: MultiEventTracer ) -> TheaterState: coalition = game.coalition_for(player) finder = ObjectiveFinder(game, player) @@ -213,8 +214,8 @@ class TheaterState(WorldState["TheaterState"]): ) ), strike_targets=list(finder.strike_targets()), - enemy_barcaps=list(game.theater.control_points_for(not player)), - threat_zones=game.threat_zone_for(not player), + enemy_barcaps=list(game.theater.control_points_for(player.opponent)), + threat_zones=game.threat_zone_for(player.opponent), vulnerable_control_points=vulnerable_control_points, control_point_priority_queue=ordered_capturable_points, priority_cp=( diff --git a/game/debriefing.py b/game/debriefing.py index e5da2add..83cf85c2 100644 --- a/game/debriefing.py +++ b/game/debriefing.py @@ -16,7 +16,7 @@ from uuid import UUID from game.dcs.aircrafttype import AircraftType from game.dcs.groundunittype import GroundUnitType -from game.theater import Airfield, ControlPoint +from game.theater import Airfield, ControlPoint, Player if TYPE_CHECKING: from game import Game @@ -45,9 +45,9 @@ class AirLosses: def losses(self) -> Iterator[FlyingUnit]: return itertools.chain(self.player, self.enemy) - def by_type(self, player: bool) -> Dict[AircraftType, int]: + def by_type(self, player: Player) -> Dict[AircraftType, int]: losses_by_type: Dict[AircraftType, int] = defaultdict(int) - losses = self.player if player else self.enemy + losses = self.player if player.is_blue else self.enemy for loss in losses: losses_by_type[loss.flight.unit_type] += 1 return losses_by_type @@ -87,7 +87,7 @@ class GroundLosses: @dataclass(frozen=True) class BaseCaptureEvent: control_point: ControlPoint - captured_by_player: bool + captured_by_player: Player @dataclass(frozen=True) @@ -166,7 +166,7 @@ class Debriefing: def merge_simulation_results(self, results: SimulationResults) -> None: for air_loss in results.air_losses: - if air_loss.flight.squadron.player: + if air_loss.flight.squadron.player.is_blue: self.air_losses.player.append(air_loss) else: self.air_losses.enemy.append(air_loss) @@ -209,9 +209,9 @@ class Debriefing: def casualty_count(self, control_point: ControlPoint) -> int: return len([x for x in self.front_line_losses if x.origin == control_point]) - def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: + def front_line_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]: losses_by_type: dict[GroundUnitType, int] = defaultdict(int) - if player: + if player.is_blue: losses = self.ground_losses.player_front_line else: losses = self.ground_losses.enemy_front_line @@ -219,9 +219,9 @@ class Debriefing: losses_by_type[loss.unit_type] += 1 return losses_by_type - def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: + def convoy_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]: losses_by_type: dict[GroundUnitType, int] = defaultdict(int) - if player: + if player.is_blue: losses = self.ground_losses.player_convoy else: losses = self.ground_losses.enemy_convoy @@ -229,9 +229,9 @@ class Debriefing: losses_by_type[loss.unit_type] += 1 return losses_by_type - def cargo_ship_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: + def cargo_ship_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]: losses_by_type: dict[GroundUnitType, int] = defaultdict(int) - if player: + if player.is_blue: ships = self.ground_losses.player_cargo_ships else: ships = self.ground_losses.enemy_cargo_ships @@ -240,9 +240,9 @@ class Debriefing: losses_by_type[unit_type] += count return losses_by_type - def airlift_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: + def airlift_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]: losses_by_type: dict[GroundUnitType, int] = defaultdict(int) - if player: + if player.is_blue: losses = self.ground_losses.player_airlifts else: losses = self.ground_losses.enemy_airlifts @@ -251,9 +251,9 @@ class Debriefing: losses_by_type[unit_type] += 1 return losses_by_type - def ground_object_losses_by_type(self, player: bool) -> Dict[str, int]: + def ground_object_losses_by_type(self, player: Player) -> Dict[str, int]: losses_by_type: Dict[str, int] = defaultdict(int) - if player: + if player.is_blue: losses = self.ground_losses.player_ground_objects else: losses = self.ground_losses.enemy_ground_objects @@ -261,9 +261,9 @@ class Debriefing: losses_by_type[loss.theater_unit.type.id] += 1 return losses_by_type - def scenery_losses_by_type(self, player: bool) -> Dict[str, int]: + def scenery_losses_by_type(self, player: Player) -> Dict[str, int]: losses_by_type: Dict[str, int] = defaultdict(int) - if player: + if player.is_blue: losses = self.ground_losses.player_scenery else: losses = self.ground_losses.enemy_scenery @@ -279,7 +279,7 @@ class Debriefing: if aircraft is None: logging.error(f"Could not find Flight matching {unit_name}") continue - if aircraft.flight.departure.captured: + if aircraft.flight.departure.captured.is_blue: player_losses.append(aircraft) else: enemy_losses.append(aircraft) @@ -290,7 +290,7 @@ class Debriefing: for unit_name in self.state_data.killed_ground_units: front_line_unit = self.unit_map.front_line_unit(unit_name) if front_line_unit is not None: - if front_line_unit.origin.captured: + if front_line_unit.origin.captured.is_blue: losses.player_front_line.append(front_line_unit) else: losses.enemy_front_line.append(front_line_unit) @@ -298,7 +298,7 @@ class Debriefing: convoy_unit = self.unit_map.convoy_unit(unit_name) if convoy_unit is not None: - if convoy_unit.convoy.player_owned: + if convoy_unit.convoy.player_owned.is_blue: losses.player_convoy.append(convoy_unit) else: losses.enemy_convoy.append(convoy_unit) @@ -306,7 +306,7 @@ class Debriefing: cargo_ship = self.unit_map.cargo_ship(unit_name) if cargo_ship is not None: - if cargo_ship.player_owned: + if cargo_ship.player_owned.is_blue: losses.player_cargo_ships.append(cargo_ship) else: losses.enemy_cargo_ships.append(cargo_ship) @@ -314,7 +314,9 @@ class Debriefing: ground_object = self.unit_map.theater_units(unit_name) if ground_object is not None: - if ground_object.theater_unit.ground_object.is_friendly(to_player=True): + if ground_object.theater_unit.ground_object.is_friendly( + to_player=Player.BLUE + ): losses.player_ground_objects.append(ground_object) else: losses.enemy_ground_objects.append(ground_object) @@ -323,7 +325,9 @@ class Debriefing: scenery_object = self.unit_map.scenery_object(unit_name) # Try appending object to the name, because we do this for building statics. if scenery_object is not None: - if scenery_object.ground_unit.ground_object.is_friendly(to_player=True): + if scenery_object.ground_unit.ground_object.is_friendly( + to_player=Player.BLUE + ): losses.player_scenery.append(scenery_object) else: losses.enemy_scenery.append(scenery_object) @@ -331,9 +335,9 @@ class Debriefing: airfield = self.unit_map.airfield(unit_name) if airfield is not None: - if airfield.captured: + if airfield.captured.is_blue: losses.player_airfields.append(airfield) - else: + elif airfield.captured.is_red: losses.enemy_airfields.append(airfield) continue @@ -349,7 +353,7 @@ class Debriefing: for unit_name in self.state_data.killed_aircraft: airlift_unit = self.unit_map.airlift_unit(unit_name) if airlift_unit is not None: - if airlift_unit.transfer.player: + if airlift_unit.transfer.player.is_blue: losses.player_airlifts.append(airlift_unit) else: losses.enemy_airlifts.append(airlift_unit) @@ -377,8 +381,10 @@ class Debriefing: # Captured base is not a part of the campaign. This happens when neutral # bases are near the conflict. Nothing to do. continue - - captured_by_player = int(new_owner_id_str) == blue_coalition_id + if int(new_owner_id_str) == blue_coalition_id: + captured_by_player = Player.BLUE + else: + captured_by_player = Player.RED if control_point.is_friendly(to_player=captured_by_player): # Base is currently friendly to the new owner. Was captured and # recaptured in the same mission. Nothing to do. diff --git a/game/game.py b/game/game.py index 33ebb046..877f82c3 100644 --- a/game/game.py +++ b/game/game.py @@ -6,7 +6,7 @@ import math from collections.abc import Iterator from datetime import date, datetime, time, timedelta from enum import Enum -from typing import Any, List, TYPE_CHECKING, Type, Union, cast +from typing import Any, List, TYPE_CHECKING, Union, cast from uuid import UUID from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers @@ -32,7 +32,7 @@ from .infos.information import Information from .lasercodes.lasercoderegistry import LaserCodeRegistry from .profiling import logged_duration from .settings import Settings -from .theater import ConflictTheater +from .theater import ConflictTheater, Player from .theater.bullseye import Bullseye from .theater.theatergroundobject import ( EwrGroundObject, @@ -138,8 +138,11 @@ class Game: self.conditions = self.generate_conditions(forced_time=start_time) self.sanitize_sides(player_faction, enemy_faction) - self.blue = Coalition(self, player_faction, player_budget, player=True) - self.red = Coalition(self, enemy_faction, enemy_budget, player=False) + self.blue = Coalition(self, player_faction, player_budget, player=Player.BLUE) + self.red = Coalition(self, enemy_faction, enemy_budget, player=Player.RED) + neutral_faction = player_faction + neutral_faction.country = self.neutral_country + self.neutral = Coalition(self, neutral_faction, 0, player=Player.NEUTRAL) self.blue.set_opponent(self.red) self.red.set_opponent(self.blue) @@ -178,10 +181,10 @@ class Game: def point_in_world(self, x: float, y: float) -> Point: return Point(x, y, self.theater.terrain) - def ato_for(self, player: bool) -> AirTaskingOrder: + def ato_for(self, player: Player) -> AirTaskingOrder: return self.coalition_for(player).ato - def transit_network_for(self, player: bool) -> TransitNetwork: + def transit_network_for(self, player: Player) -> TransitNetwork: return self.coalition_for(player).transit_network def generate_conditions(self, forced_time: time | None = None) -> Conditions: @@ -209,32 +212,35 @@ class Game: else: enemy_faction.country = country_with_name("Russia") - def faction_for(self, player: bool) -> Faction: + def faction_for(self, player: Player) -> Faction: return self.coalition_for(player).faction - def faker_for(self, player: bool) -> Faker: + def faker_for(self, player: Player) -> Faker: return self.coalition_for(player).faker - def air_wing_for(self, player: bool) -> AirWing: + def air_wing_for(self, player: Player) -> AirWing: return self.coalition_for(player).air_wing @property - def neutral_country(self) -> Type[Country]: + def neutral_country(self) -> Country: """Return the best fitting country that can be used as neutral faction in the generated mission""" countries_in_use = {self.red.faction.country, self.blue.faction.country} if UnitedNationsPeacekeepers not in countries_in_use: - return UnitedNationsPeacekeepers + return UnitedNationsPeacekeepers() elif Switzerland.name not in countries_in_use: - return Switzerland + return Switzerland() else: - return USAFAggressors + return USAFAggressors() - def coalition_for(self, player: bool) -> Coalition: - if player: + def coalition_for(self, player: Player) -> Coalition: + if player.is_neutral: + return self.neutral + elif player.is_blue: return self.blue - return self.red + else: + return self.red - def adjust_budget(self, amount: float, player: bool) -> None: + def adjust_budget(self, amount: float, player: Player) -> None: self.coalition_for(player).adjust_budget(amount) def on_load(self, game_still_initializing: bool = False) -> None: @@ -489,7 +495,7 @@ class Game: self.current_group_id += 1 return self.current_group_id - def compute_transit_network_for(self, player: bool) -> TransitNetwork: + def compute_transit_network_for(self, player: Player) -> TransitNetwork: return TransitNetworkBuilder(self.theater, player).build() def compute_threat_zones(self, events: GameUpdateEvents) -> None: @@ -498,10 +504,10 @@ class Game: self.blue.compute_nav_meshes(events) self.red.compute_nav_meshes(events) - def threat_zone_for(self, player: bool) -> ThreatZones: + def threat_zone_for(self, player: Player) -> ThreatZones: return self.coalition_for(player).threat_zone - def navmesh_for(self, player: bool) -> NavMesh: + def navmesh_for(self, player: Player) -> NavMesh: return self.coalition_for(player).nav_mesh def compute_unculled_zones(self, events: GameUpdateEvents) -> None: diff --git a/game/groundunitorders.py b/game/groundunitorders.py index b6644527..29358586 100644 --- a/game/groundunitorders.py +++ b/game/groundunitorders.py @@ -66,7 +66,7 @@ class GroundUnitOrders: bought_units: dict[GroundUnitType, int] = {} units_needing_transfer: dict[GroundUnitType, int] = {} for unit_type, count in self.units.items(): - allegiance = "Ally" if self.destination.captured else "Enemy" + allegiance = "Ally" if self.destination.captured.is_blue else "Enemy" d: dict[GroundUnitType, int] if self.destination != ground_unit_source: source = ground_unit_source diff --git a/game/income.py b/game/income.py index 729ecd7a..22891288 100644 --- a/game/income.py +++ b/game/income.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING from game.config import REWARDS +from game.theater.player import Player if TYPE_CHECKING: from game import Game @@ -22,8 +23,8 @@ class BuildingIncome: class Income: - def __init__(self, game: Game, player: bool) -> None: - if player: + def __init__(self, game: Game, player: Player) -> None: + if player.is_blue: self.multiplier = game.settings.player_income_multiplier else: self.multiplier = game.settings.enemy_income_multiplier diff --git a/game/missiongenerator/aircraft/aircraftgenerator.py b/game/missiongenerator/aircraft/aircraftgenerator.py index 6f4cc57c..883ded8d 100644 --- a/game/missiongenerator/aircraft/aircraftgenerator.py +++ b/game/missiongenerator/aircraft/aircraftgenerator.py @@ -220,7 +220,7 @@ class AircraftGenerator: ): continue - if control_point.captured: + if control_point.captured.is_blue: country = player_country else: country = enemy_country @@ -237,12 +237,12 @@ class AircraftGenerator: squadron.location, Fob ) if ( - squadron.coalition.player + squadron.coalition.player.is_blue and self.game.settings.perf_disable_untasked_blufor_aircraft ): return elif ( - not squadron.coalition.player + not squadron.coalition.player.is_red and self.game.settings.perf_disable_untasked_opfor_aircraft ): return @@ -274,7 +274,7 @@ class AircraftGenerator: ).create_idle_aircraft() if group: if ( - not squadron.coalition.player + squadron.coalition.player.is_red and squadron.aircraft.flyable and ( self.game.settings.enable_squadron_pilot_limits diff --git a/game/missiongenerator/aircraft/flightdata.py b/game/missiongenerator/aircraft/flightdata.py index 27d1e408..1a94cc8d 100644 --- a/game/missiongenerator/aircraft/flightdata.py +++ b/game/missiongenerator/aircraft/flightdata.py @@ -14,6 +14,7 @@ if TYPE_CHECKING: from game.dcs.aircrafttype import AircraftType from game.radio.radios import RadioFrequency from game.runways import RunwayData + from game.theater.player import Player @dataclass(frozen=True) @@ -42,7 +43,7 @@ class FlightData: size: int #: True if this flight belongs to the player's coalition. - friendly: bool + friendly: Player #: Number of seconds after mission start the flight is set to depart. departure_delay: timedelta diff --git a/game/missiongenerator/aircraft/flightgroupconfigurator.py b/game/missiongenerator/aircraft/flightgroupconfigurator.py index 9555e3e6..080cf5bc 100644 --- a/game/missiongenerator/aircraft/flightgroupconfigurator.py +++ b/game/missiongenerator/aircraft/flightgroupconfigurator.py @@ -299,7 +299,7 @@ class FlightGroupConfigurator: unit.set_player() def skill_level_for(self, unit: FlyingUnit, pilot: Optional[Pilot]) -> Skill: - if self.flight.squadron.player: + if self.flight.squadron.player.is_blue: base_skill = Skill(self.game.settings.player_skill) else: base_skill = Skill(self.game.settings.enemy_skill) diff --git a/game/missiongenerator/briefinggenerator.py b/game/missiongenerator/briefinggenerator.py index 733a4676..356ffe10 100644 --- a/game/missiongenerator/briefinggenerator.py +++ b/game/missiongenerator/briefinggenerator.py @@ -17,8 +17,8 @@ from game.radio.radios import RadioFrequency from game.runways import RunwayData from game.theater import ControlPoint, FrontLine from .aircraft.flightdata import FlightData -from .missiondata import AwacsInfo, TankerInfo from .flotgenerator import JtacInfo +from .missiondata import AwacsInfo, TankerInfo if TYPE_CHECKING: from game import Game @@ -182,7 +182,7 @@ class BriefingGenerator(MissionInfoGenerator): def generate_allied_flights_by_departure(self) -> None: """Create iterable to display allied flights grouped by departure airfield.""" for flight in self.flights: - if not flight.client_units and flight.friendly: + if not flight.client_units and flight.friendly.is_blue: name = flight.departure.airfield_name if ( name in self.allied_flights_by_departure diff --git a/game/missiongenerator/convoygenerator.py b/game/missiongenerator/convoygenerator.py index e1bad3ce..eaf4118c 100644 --- a/game/missiongenerator/convoygenerator.py +++ b/game/missiongenerator/convoygenerator.py @@ -18,6 +18,7 @@ from game.utils import kph if TYPE_CHECKING: from game import Game + from game.theater.player import Player class ConvoyGenerator: @@ -94,7 +95,7 @@ class ConvoyGenerator: name: str, position: Point, units: dict[GroundUnitType, int], - for_player: bool, + for_player: Player, ) -> VehicleGroup: unit_types = list(units.items()) main_unit_type, main_unit_count = unit_types[0] diff --git a/game/missiongenerator/drawingsgenerator.py b/game/missiongenerator/drawingsgenerator.py index a7db3fd5..77bf5b9a 100644 --- a/game/missiongenerator/drawingsgenerator.py +++ b/game/missiongenerator/drawingsgenerator.py @@ -15,6 +15,7 @@ FRONTLINE_COLORS = Rgba(255, 0, 0, 255) WHITE = Rgba(255, 255, 255, 255) CP_RED = Rgba(255, 0, 0, 80) CP_BLUE = Rgba(0, 0, 255, 80) +CP_NEUTRAL = Rgba(128, 128, 128, 80) BLUE_PATH_COLOR = Rgba(0, 0, 255, 100) RED_PATH_COLOR = Rgba(255, 0, 0, 100) ACTIVE_PATH_COLOR = Rgba(255, 80, 80, 100) @@ -35,10 +36,12 @@ class DrawingsGenerator: Generate cps as circles """ for cp in self.game.theater.controlpoints: - if cp.captured: + if cp.captured.is_blue: color = CP_BLUE - else: + elif cp.captured.is_red: color = CP_RED + else: + color = CP_NEUTRAL shape = self.player_layer.add_circle( cp.position, TRIGGER_RADIUS_CAPTURE, @@ -61,9 +64,9 @@ class DrawingsGenerator: continue else: # Determine path color - if cp.captured and destination.captured: + if cp.captured.is_blue and destination.captured.is_blue: color = BLUE_PATH_COLOR - elif not cp.captured and not destination.captured: + elif cp.captured.is_red and destination.captured.is_red: color = RED_PATH_COLOR else: color = ACTIVE_PATH_COLOR diff --git a/game/missiongenerator/flotgenerator.py b/game/missiongenerator/flotgenerator.py index 89be57e7..f95bde7b 100644 --- a/game/missiongenerator/flotgenerator.py +++ b/game/missiongenerator/flotgenerator.py @@ -40,7 +40,7 @@ from game.ground_forces.ai_ground_planner import ( from game.ground_forces.combat_stance import CombatStance from game.naming import namegen from game.radio.radios import RadioRegistry -from game.theater.controlpoint import ControlPoint +from game.theater.controlpoint import ControlPoint, Player from game.unitmap import UnitMap from game.utils import Heading from .frontlineconflictdescription import FrontLineConflictDescription @@ -101,12 +101,12 @@ class FlotGenerator: # Create player groups at random position player_groups = self._generate_groups( - self.player_planned_combat_groups, is_player=True + self.player_planned_combat_groups, is_player=Player.BLUE ) # Create enemy groups at random position enemy_groups = self._generate_groups( - self.enemy_planned_combat_groups, is_player=False + self.enemy_planned_combat_groups, is_player=Player.RED ) # TODO: Differentiate AirConflict and GroundConflict classes. @@ -193,7 +193,7 @@ class FlotGenerator: callsign=callsign, region=frontline, code=str(code), - blue=True, + blue=Player.BLUE, freq=freq, ) ) @@ -215,7 +215,7 @@ class FlotGenerator: def gen_infantry_group_for_group( self, group: VehicleGroup, - is_player: bool, + is_player: Player, side: Country, forward_heading: Heading, ) -> None: @@ -294,7 +294,7 @@ class FlotGenerator: GroundForcePainter(faction, vehicle).apply_livery() vg.hidden_on_mfd = True - def _earliest_tot_on_flot(self, player: bool) -> timedelta: + def _earliest_tot_on_flot(self, player: Player) -> timedelta: tots = [ x.time_over_target for x in self.game.ato_for(player).packages @@ -413,7 +413,7 @@ class FlotGenerator: """ duration = timedelta() if stance in [CombatStance.DEFENSIVE, CombatStance.AGGRESSIVE]: - duration = self._earliest_tot_on_flot(not to_cp.coalition.player) + duration = self._earliest_tot_on_flot(to_cp.coalition.player.opponent) self._set_reform_waypoint(dcs_group, forward_heading, duration) if stance == CombatStance.AGGRESSIVE: # Attack nearest enemy if any @@ -503,7 +503,7 @@ class FlotGenerator: """ duration = timedelta() if stance in [CombatStance.DEFENSIVE, CombatStance.AGGRESSIVE]: - duration = self._earliest_tot_on_flot(not to_cp.coalition.player) + duration = self._earliest_tot_on_flot(to_cp.coalition.player.opponent) self._set_reform_waypoint(dcs_group, forward_heading, duration) if stance in [ CombatStance.AGGRESSIVE, @@ -762,7 +762,7 @@ class FlotGenerator: ) def _generate_groups( - self, groups: list[CombatGroup], is_player: bool + self, groups: list[CombatGroup], is_player: Player ) -> List[Tuple[VehicleGroup, CombatGroup]]: """Finds valid positions for planned groups and generates a pydcs group for them""" positioned_groups = [] @@ -795,7 +795,7 @@ class FlotGenerator: final_position, heading=spawn_heading.opposite, ) - if is_player: + if is_player == Player.BLUE: g.set_skill(Skill(self.game.settings.player_skill)) else: g.set_skill(Skill(self.game.settings.enemy_vehicle_skill)) @@ -813,7 +813,7 @@ class FlotGenerator: def _generate_group( self, - player: bool, + player: Player, side: Country, unit_type: GroundUnitType, count: int, diff --git a/game/missiongenerator/luagenerator.py b/game/missiongenerator/luagenerator.py index 9c8cca52..209a4b64 100644 --- a/game/missiongenerator/luagenerator.py +++ b/game/missiongenerator/luagenerator.py @@ -10,15 +10,14 @@ from dcs import Mission from dcs.action import DoScript, DoScriptFile from dcs.translation import String from dcs.triggers import TriggerStart -from dcs.unit import Skill from game.ato import FlightType +from game.data.units import UnitClass from game.dcs.aircrafttype import AircraftType from game.plugins import LuaPluginManager from game.theater import TheaterGroundObject from game.theater.iadsnetwork.iadsrole import IadsRole from game.utils import escape_string_for_lua -from game.data.units import UnitClass from .missiondata import MissionData if TYPE_CHECKING: @@ -144,7 +143,7 @@ class LuaGenerator: target_points = lua_data.add_item("TargetPoints") for flight in self.mission_data.flights: - if flight.friendly and flight.flight_type in [ + if flight.friendly.is_blue and flight.flight_type in [ FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, @@ -178,7 +177,7 @@ class LuaGenerator: for cp in self.game.theater.controlpoints: coalition_object = ( lua_data.get_or_create_item("BlueAA") - if cp.captured + if cp.captured.is_blue else lua_data.get_or_create_item("RedAA") ) for ground_object in cp.ground_objects: diff --git a/game/missiongenerator/missiondata.py b/game/missiongenerator/missiondata.py index cbcac39c..aadd003d 100644 --- a/game/missiongenerator/missiondata.py +++ b/game/missiongenerator/missiondata.py @@ -15,6 +15,7 @@ from game.runways import RunwayData if TYPE_CHECKING: from game.radio.radios import RadioFrequency from game.radio.tacan import TacanChannel + from game.theater.player import Player from game.utils import Distance from uuid import UUID @@ -24,7 +25,7 @@ class GroupInfo: group_name: str callsign: str freq: RadioFrequency - blue: bool + blue: Player @dataclass @@ -85,7 +86,7 @@ class LogisticsInfo: pilot_names: list[str] transport: AircraftType - blue: bool + blue: Player logistic_unit: str = field(default_factory=str) pickup_zone: str = field(default_factory=str) diff --git a/game/missiongenerator/missiongenerator.py b/game/missiongenerator/missiongenerator.py index f9782118..128d31b6 100644 --- a/game/missiongenerator/missiongenerator.py +++ b/game/missiongenerator/missiongenerator.py @@ -391,9 +391,12 @@ class MissionGenerator: tmu.theater_unit.position, self.mission.terrain, ).dict() - warehouse["coalition"] = ( - "blue" if tmu.theater_unit.ground_object.coalition.player else "red" - ) + if tmu.theater_unit.ground_object.coalition.player.is_neutral: + warehouse["coalition"] = "neutral" + elif tmu.theater_unit.ground_object.coalition.player.is_blue: + warehouse["coalition"] = "blue" + else: + warehouse["coalition"] = "red" warehouse["dynamicCargo"] = settings.dynamic_cargo if tmu.theater_unit.is_ship or tmu.dcs_unit.category == "Heliports": # type: ignore warehouse["dynamicSpawn"] = settings.dynamic_slots diff --git a/game/missiongenerator/rebelliongenerator.py b/game/missiongenerator/rebelliongenerator.py index c664aafa..3b6fd5a8 100644 --- a/game/missiongenerator/rebelliongenerator.py +++ b/game/missiongenerator/rebelliongenerator.py @@ -13,6 +13,7 @@ from dcs.vehicles import vehicle_map from game.dcs.groundunittype import GroundUnitType from game.naming import namegen +from game.theater import Player if TYPE_CHECKING: from game import Game @@ -25,12 +26,12 @@ class RebellionGenerator: def generate(self) -> None: ownfor_country = self.mission.country( - self.game.coalition_for(player=True).faction.country.name + self.game.coalition_for(player=Player.BLUE).faction.country.name ) for rz in self.game.theater.ownfor_rebel_zones: self._generate_rebel_zone(ownfor_country, rz) opfor_country = self.mission.country( - self.game.coalition_for(player=False).faction.country.name + self.game.coalition_for(player=Player.RED).faction.country.name ) for rz in self.game.theater.opfor_rebel_zones: self._generate_rebel_zone(opfor_country, rz) diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py index 1ce0ee36..da7cb4ef 100644 --- a/game/missiongenerator/tgogenerator.py +++ b/game/missiongenerator/tgogenerator.py @@ -67,6 +67,7 @@ from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage from game.runways import RunwayData from game.theater import ( ControlPoint, + Player, TheaterGroundObject, TheaterUnit, NavalControlPoint, @@ -408,7 +409,7 @@ class GroundObjectGenerator: # Align the trigger zones to the faction color on the DCS briefing/F10 map. color = ( {1: 0.2, 2: 0.7, 3: 1, 4: 0.15} - if scenery.ground_object.is_friendly(to_player=True) + if scenery.ground_object.is_friendly(to_player=Player.BLUE) else {1: 1, 2: 0.2, 3: 0.2, 4: 0.15} ) @@ -878,7 +879,12 @@ class HelipadGenerator: pad.position, self.m.terrain, ).dict() - warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" + if self.cp.coalition.player.is_neutral: + warehouse["coalition"] = "neutral" + elif self.cp.coalition.player.is_blue: + warehouse["coalition"] = "blue" + else: + warehouse["coalition"] = "red" # configure dynamic spawn + hot start of DS, plus dynamic cargo? self.m.warehouses.warehouses[pad.id] = warehouse @@ -1005,7 +1011,12 @@ class GroundSpawnRoadbaseGenerator: pad.position, self.m.terrain, ).dict() - warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" + if self.cp.coalition.player.is_neutral: + warehouse["coalition"] = "neutral" + elif self.cp.coalition.player.is_blue: + warehouse["coalition"] = "blue" + else: + warehouse["coalition"] = "red" # configure dynamic spawn + hot start of DS, plus dynamic cargo? self.m.warehouses.warehouses[pad.id] = warehouse @@ -1131,7 +1142,12 @@ class GroundSpawnLargeGenerator: pad.position, self.m.terrain, ).dict() - warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" + if self.cp.coalition.player.is_neutral: + warehouse["coalition"] = "neutral" + elif self.cp.coalition.player.is_blue: + warehouse["coalition"] = "blue" + else: + warehouse["coalition"] = "red" # configure dynamic spawn + hot start of DS, plus dynamic cargo? self.m.warehouses.warehouses[pad.id] = warehouse @@ -1275,7 +1291,12 @@ class GroundSpawnGenerator: pad.position, self.m.terrain, ).dict() - warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" + if self.cp.coalition.player.is_neutral: + warehouse["coalition"] = "neutral" + elif self.cp.coalition.player.is_blue: + warehouse["coalition"] = "blue" + else: + warehouse["coalition"] = "red" # configure dynamic spawn + hot start of DS, plus dynamic cargo? self.m.warehouses.warehouses[pad.id] = warehouse diff --git a/game/missiongenerator/triggergenerator.py b/game/missiongenerator/triggergenerator.py index cc31b4e5..bb648488 100644 --- a/game/missiongenerator/triggergenerator.py +++ b/game/missiongenerator/triggergenerator.py @@ -12,7 +12,6 @@ from dcs.action import ( RemoveSceneObjects, RemoveSceneObjectsMask, SceneryDestructionZone, - Smoke, ) from dcs.condition import ( AllOfCoalitionOutsideZone, @@ -99,9 +98,12 @@ class TriggerGenerator: raise RuntimeError( f"Could not find {airfield.airport.name} in the mission" ) - cp_airport.set_coalition( - airfield.captured and player_coalition or enemy_coalition - ) + if airfield.captured.is_neutral: + cp_airport.set_coalition("neutral") + elif airfield.captured.is_blue: + cp_airport.set_coalition(player_coalition) + elif airfield.captured.is_red: + cp_airport.set_coalition(enemy_coalition) def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None: """ @@ -138,7 +140,7 @@ class TriggerGenerator: zone = self.mission.triggers.add_triggerzone( location, radius=10, hidden=True, name="MARK" ) - if cp.captured: + if cp.captured.is_blue: name = ground_object.obj_name + " [ALLY]" else: name = ground_object.obj_name + " [ENEMY]" @@ -186,7 +188,7 @@ class TriggerGenerator: """ for cp in self.game.theater.controlpoints: if isinstance(cp, self.capture_zone_types) and not cp.is_carrier: - if cp.captured: + if cp.captured.is_blue: attacking_coalition = enemy_coalition attack_coalition_int = 1 # 1 is the Event int for Red defending_coalition = player_coalition @@ -242,6 +244,51 @@ class TriggerGenerator: recapture_trigger.add_action(ClearFlag(flag=flag)) self.mission.triggerrules.triggers.append(recapture_trigger) + if cp.captured.is_neutral: + red_capture_trigger = TriggerCondition( + Event.NoEvent, "Capture Trigger" + ) + red_capture_trigger.add_condition( + AllOfCoalitionOutsideZone( + attacking_coalition, trigger_zone.id, unit_type="GROUND" + ) + ) + red_capture_trigger.add_condition( + PartOfCoalitionInZone( + defending_coalition, trigger_zone.id, unit_type="GROUND" + ) + ) + red_capture_trigger.add_condition(FlagIsFalse(flag=flag)) + script_string = String( + f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{defend_coalition_int}||{cp.full_name}"' + ) + red_capture_trigger.add_action(DoScript(script_string)) + red_capture_trigger.add_action(SetFlag(flag=flag)) + self.mission.triggerrules.triggers.append(red_capture_trigger) + + inverted_recapture_trigger = TriggerCondition( + Event.NoEvent, "Capture Trigger" + ) + inverted_recapture_trigger.add_condition( + AllOfCoalitionOutsideZone( + defending_coalition, trigger_zone.id, unit_type="GROUND" + ) + ) + inverted_recapture_trigger.add_condition( + PartOfCoalitionInZone( + attacking_coalition, trigger_zone.id, unit_type="GROUND" + ) + ) + inverted_recapture_trigger.add_condition(FlagIsTrue(flag=flag)) + script_string = String( + f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{attack_coalition_int}||{cp.full_name}"' + ) + inverted_recapture_trigger.add_action(DoScript(script_string)) + inverted_recapture_trigger.add_action(ClearFlag(flag=flag)) + self.mission.triggerrules.triggers.append( + inverted_recapture_trigger + ) + def generate(self) -> None: player_coalition = "blue" enemy_coalition = "red" diff --git a/game/models/game_stats.py b/game/models/game_stats.py index 780565d4..426e519d 100644 --- a/game/models/game_stats.py +++ b/game/models/game_stats.py @@ -55,7 +55,7 @@ class GameStats: turn_data = GameTurnMetadata() for cp in game.theater.controlpoints: - if cp.captured: + if cp.captured.is_blue: 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()) diff --git a/game/pretense/pretenseaircraftgenerator.py b/game/pretense/pretenseaircraftgenerator.py index 60d444f3..060f6860 100644 --- a/game/pretense/pretenseaircraftgenerator.py +++ b/game/pretense/pretenseaircraftgenerator.py @@ -35,6 +35,7 @@ from game.runways import RunwayData from game.settings import Settings from game.squadrons import AirWing from game.squadrons import Squadron +from game.theater.player import Player from game.theater.controlpoint import ( ControlPoint, OffMapSpawn, @@ -833,7 +834,7 @@ class PretenseAircraftGenerator: """ self.initialize_pretense_data_structures(cp) - is_player = True + is_player = Player.BLUE if country == cp.coalition.faction.country: offmap_transport_cp = self.find_pretense_cargo_plane_cp(cp) @@ -868,7 +869,7 @@ class PretenseAircraftGenerator: coalition = ( self.game.coalition_for(is_player) if country == self.game.coalition_for(is_player).faction.country - else self.game.coalition_for(False) + else self.game.coalition_for(Player.RED) ) self.generate_pretense_aircraft_for_other_side(cp, coalition, ato) diff --git a/game/pretense/pretenseflightgroupspawner.py b/game/pretense/pretenseflightgroupspawner.py index ace1b3f2..d7f421bb 100644 --- a/game/pretense/pretenseflightgroupspawner.py +++ b/game/pretense/pretenseflightgroupspawner.py @@ -25,7 +25,7 @@ from game.missiongenerator.aircraft.flightgroupspawner import ( ) from game.missiongenerator.missiondata import MissionData from game.naming import NameGenerator -from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint +from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, Player class PretenseNameGenerator(NameGenerator): @@ -87,7 +87,7 @@ class PretenseFlightGroupSpawner(FlightGroupSpawner): def insert_into_pretense(self, name: str) -> None: cp = self.flight.departure - is_player = True + is_player = Player.BLUE cp_side = ( 2 if self.flight.coalition diff --git a/game/pretense/pretenseluagenerator.py b/game/pretense/pretenseluagenerator.py index e19e8fcb..07948d66 100644 --- a/game/pretense/pretenseluagenerator.py +++ b/game/pretense/pretenseluagenerator.py @@ -1495,13 +1495,13 @@ class PretenseLuaGenerator(LuaGenerator): cp_name.replace("ä", "a") cp_name.replace("ö", "o") cp_name.replace("ø", "o") - cp_side = 2 if cp.captured else 1 + cp_side = 2 if cp.captured.is_blue else 1 if isinstance(cp, OffMapSpawn): continue elif ( cp.is_fleet - and cp.captured + and cp.captured.is_blue and self.game.settings.pretense_controllable_carrier ): # Friendly carrier, generate carrier parameters @@ -1591,7 +1591,7 @@ class PretenseLuaGenerator(LuaGenerator): # Also connect carrier and LHA control points to adjacent friendly points if cp.is_fleet and ( not self.game.settings.pretense_controllable_carrier - or not cp.captured + or cp.captured.is_red ): num_of_carrier_connections = 0 for ( @@ -1616,7 +1616,7 @@ class PretenseLuaGenerator(LuaGenerator): try: if ( cp.is_fleet - and cp.captured + and cp.captured.is_blue and self.game.settings.pretense_controllable_carrier ): break diff --git a/game/pretense/pretensemissiongenerator.py b/game/pretense/pretensemissiongenerator.py index cb94b898..f894542c 100644 --- a/game/pretense/pretensemissiongenerator.py +++ b/game/pretense/pretensemissiongenerator.py @@ -28,6 +28,7 @@ from game.missiongenerator.visualsgenerator import VisualsGenerator from game.naming import namegen from game.persistency import pre_pretense_backups_dir from game.pretense.pretenseaircraftgenerator import PretenseAircraftGenerator +from game.theater import Player from game.theater.bullseye import Bullseye from game.unitmap import UnitMap from qt_ui.windows.GameUpdateSignal import GameUpdateSignal @@ -233,7 +234,7 @@ class PretenseMissionGenerator(MissionGenerator): callsign=callsign, region=frontline, code=str(code), - blue=True, + blue=Player.BLUE, freq=freq, ) ) diff --git a/game/pretense/pretensetgogenerator.py b/game/pretense/pretensetgogenerator.py index 75a1cf08..50d68969 100644 --- a/game/pretense/pretensetgogenerator.py +++ b/game/pretense/pretensetgogenerator.py @@ -49,6 +49,7 @@ from game.theater import ( TheaterUnit, NavalControlPoint, PresetLocation, + Player, ) from game.theater.theatergroundobject import ( CarrierGroundObject, @@ -248,7 +249,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator): """ unit_type = None faction = self.coalition.faction - is_player = True + is_player = Player.BLUE side = ( 2 if self.country == self.game.coalition_for(is_player).faction.country @@ -458,7 +459,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator): cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( control_point.name ) - is_player = True + is_player = Player.BLUE side = ( 2 if self.country @@ -567,7 +568,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator): cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( control_point.name ) - is_player = True + is_player = Player.BLUE side = ( 2 if self.country == self.game.coalition_for(is_player).faction.country diff --git a/game/pretense/pretensetriggergenerator.py b/game/pretense/pretensetriggergenerator.py index 1f1bd829..783827eb 100644 --- a/game/pretense/pretensetriggergenerator.py +++ b/game/pretense/pretensetriggergenerator.py @@ -1,9 +1,7 @@ from __future__ import annotations -import logging -import math import random -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from dcs import Point from dcs.action import ( @@ -11,10 +9,6 @@ from dcs.action import ( DoScript, MarkToAll, SetFlag, - RemoveSceneObjects, - RemoveSceneObjectsMask, - SceneryDestructionZone, - Smoke, ) from dcs.condition import ( AllOfCoalitionOutsideZone, @@ -22,7 +16,6 @@ from dcs.condition import ( FlagIsTrue, PartOfCoalitionInZone, TimeAfter, - TimeSinceFlag, ) from dcs.mission import Mission from dcs.task import Option @@ -31,12 +24,11 @@ from dcs.terrain.syria.airports import Damascus, Khalkhalah from dcs.translation import String from dcs.triggers import Event, TriggerCondition, TriggerOnce from dcs.unit import Skill -from numpy import cross, einsum, arctan2 from shapely import MultiPolygon, Point as ShapelyPoint from game.naming import ALPHA_MILITARY from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator -from game.theater import Airfield +from game.theater import Airfield, Player from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE, OffMapSpawn if TYPE_CHECKING: @@ -157,7 +149,7 @@ class PretenseTriggerGenerator: zone = self.mission.triggers.add_triggerzone( location, radius=10, hidden=True, name="MARK" ) - if cp.captured: + if cp.captured.is_blue: name = ground_object.obj_name + " [ALLY]" else: name = ground_object.obj_name + " [ENEMY]" @@ -174,7 +166,7 @@ class PretenseTriggerGenerator: """ for cp in self.game.theater.controlpoints: if isinstance(cp, self.capture_zone_types) and not cp.is_carrier: - if cp.captured: + if cp.captured.is_blue: attacking_coalition = enemy_coalition attack_coalition_int = 1 # 1 is the Event int for Red defending_coalition = player_coalition @@ -243,7 +235,7 @@ class PretenseTriggerGenerator: self.game.settings.pretense_carrier_zones_navmesh == "Blue navmesh" ) sea_zones_landmap = self.game.coalition_for( - player=False + player=Player.RED ).nav_mesh.theater.landmap if ( self.game.settings.pretense_controllable_carrier @@ -251,7 +243,7 @@ class PretenseTriggerGenerator: ): navmesh_number = 0 for navmesh_poly in self.game.coalition_for( - player=use_blue_navmesh + player=Player.BLUE if use_blue_navmesh else Player.RED ).nav_mesh.polys: navmesh_number += 1 if sea_zones_landmap.sea_zones.intersects(navmesh_poly.poly): @@ -325,7 +317,7 @@ class PretenseTriggerGenerator: if ( cp.is_fleet and self.game.settings.pretense_controllable_carrier - and cp.captured + and cp.captured.is_blue ): # Friendly carrier zones are generated above continue diff --git a/game/procurement.py b/game/procurement.py index 947c0202..47ffcbf1 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -8,7 +8,7 @@ from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple from game.config import RUNWAY_REPAIR_COST from game.data.units import UnitClass from game.dcs.groundunittype import GroundUnitType -from game.theater import ControlPoint, MissionTarget, ParkingType +from game.theater import ControlPoint, MissionTarget, ParkingType, Player if TYPE_CHECKING: from game import Game @@ -34,20 +34,20 @@ class ProcurementAi: def __init__( self, game: Game, - for_player: bool, + owner: Player, faction: Faction, manage_runways: bool, manage_front_line: bool, manage_aircraft: bool, ) -> None: self.game = game - self.is_player = for_player - self.air_wing = game.air_wing_for(for_player) + self.is_player = owner + self.air_wing = game.air_wing_for(owner) self.faction = faction self.manage_runways = manage_runways self.manage_front_line = manage_front_line self.manage_aircraft = manage_aircraft - self.threat_zones = self.game.threat_zone_for(not self.is_player) + self.threat_zones = self.game.threat_zone_for(self.is_player.opponent) def calculate_ground_unit_budget_share(self) -> float: armor_investment = 0 @@ -114,7 +114,7 @@ class ProcurementAi: if control_point.runway_can_be_repaired: control_point.begin_runway_repair() budget -= RUNWAY_REPAIR_COST - if self.is_player: + if self.is_player.is_blue: self.game.message( "We have begun repairing the runway at " f"{control_point}" ) @@ -223,7 +223,7 @@ class ProcurementAi: @property def owned_points(self) -> List[ControlPoint]: - if self.is_player: + if self.is_player.is_blue: return self.game.theater.player_points() else: return self.game.theater.enemy_points() diff --git a/game/server/controlpoints/models.py b/game/server/controlpoints/models.py index a0a22e76..2d8c4f4d 100644 --- a/game/server/controlpoints/models.py +++ b/game/server/controlpoints/models.py @@ -29,12 +29,16 @@ class ControlPointJs(BaseModel): destination = None if control_point.target_position is not None: destination = control_point.target_position.latlng() + if control_point.captured.is_blue: + blue = True + else: + blue = False return ControlPointJs( id=control_point.id, name=control_point.name, - blue=control_point.captured, + blue=blue, position=control_point.position.latlng(), - mobile=control_point.moveable and control_point.captured, + mobile=control_point.moveable and control_point.captured.is_blue, destination=destination, sidc=str(control_point.sidc()), ) diff --git a/game/server/controlpoints/routes.py b/game/server/controlpoints/routes.py index 4274f628..cac1dfc8 100644 --- a/game/server/controlpoints/routes.py +++ b/game/server/controlpoints/routes.py @@ -6,6 +6,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, status from starlette.responses import Response from game import Game +from game.theater.player import Player from .models import ControlPointJs from ..dependencies import GameContext from ..leaflet import LeafletPoint @@ -75,7 +76,7 @@ def set_destination( ) if not cp.moveable: raise HTTPException(status.HTTP_403_FORBIDDEN, detail=f"{cp} is not mobile") - if not cp.captured: + if not cp.captured.is_blue: raise HTTPException( status.HTTP_403_FORBIDDEN, detail=f"{cp} is not owned by the player" ) @@ -120,7 +121,7 @@ def cancel_travel(cp_id: UUID, game: Game = Depends(GameContext.require)) -> Non ) if not cp.moveable: raise HTTPException(status.HTTP_403_FORBIDDEN, detail=f"{cp} is not mobile") - if not cp.captured: + if not cp.captured.is_blue: raise HTTPException( status.HTTP_403_FORBIDDEN, detail=f"{cp} is not owned by the player" ) diff --git a/game/server/eventstream/models.py b/game/server/eventstream/models.py index 11d82c38..b9b861a6 100644 --- a/game/server/eventstream/models.py +++ b/game/server/eventstream/models.py @@ -15,6 +15,7 @@ from game.server.mapzones.models import ThreatZonesJs, UnculledZoneJs from game.server.navmesh.models import NavMeshJs from game.server.supplyroutes.models import SupplyRouteJs from game.server.tgos.models import TgoJs +from game.theater import Player if TYPE_CHECKING: from game import Game @@ -26,9 +27,9 @@ class GameUpdateEventsJs(BaseModel): new_combats: list[FrozenCombatJs] updated_combats: list[FrozenCombatJs] ended_combats: list[UUID] - navmesh_updates: dict[bool, NavMeshJs] + navmesh_updates: dict[Player, NavMeshJs] updated_unculled_zones: list[UnculledZoneJs] - threat_zones_updated: dict[bool, ThreatZonesJs] + threat_zones_updated: dict[Player, ThreatZonesJs] new_flights: list[FlightJs] updated_flights: list[FlightJs] deleted_flights: set[UUID] diff --git a/game/server/flights/models.py b/game/server/flights/models.py index bd965c51..1a3a7ee8 100644 --- a/game/server/flights/models.py +++ b/game/server/flights/models.py @@ -41,9 +41,13 @@ class FlightJs(BaseModel): waypoints = None if with_waypoints: waypoints = waypoints_for_flight(flight) + if flight.blue.is_blue: + blue = True + else: + blue = False return FlightJs( id=flight.id, - blue=flight.blue, + blue=blue, position=position, sidc=str(flight.sidc()), waypoints=waypoints, diff --git a/game/server/iadsnetwork/models.py b/game/server/iadsnetwork/models.py index 6a4a4fea..0e3b50fe 100644 --- a/game/server/iadsnetwork/models.py +++ b/game/server/iadsnetwork/models.py @@ -5,6 +5,7 @@ from uuid import UUID from pydantic import BaseModel from game.server.leaflet import LeafletPoint +from game.theater.player import Player from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode, IadsNetwork @@ -34,8 +35,16 @@ class IadsConnectionJs(BaseModel): iads_connections = [] tgo = network_node.group.ground_object for id, connection in network_node.connections.items(): - if connection.ground_object.is_friendly(True) != tgo.is_friendly(True): + if connection.ground_object.is_friendly(Player.BLUE) != tgo.is_friendly( + Player.BLUE + ): continue # Skip connections which are not from same coalition + if tgo.is_friendly(Player.BLUE): + blue = True + elif tgo.is_friendly(Player.RED): + blue = False + else: + continue # Skip neutral iads_connections.append( IadsConnectionJs( id=id, @@ -49,7 +58,7 @@ class IadsConnectionJs(BaseModel): network_node.group.alive_units > 0 and connection.alive_units > 0 ), - blue=tgo.is_friendly(True), + blue=blue, is_power="power" in [tgo.category, connection.ground_object.category], ) diff --git a/game/server/mapzones/models.py b/game/server/mapzones/models.py index 33eb6ba6..0607280b 100644 --- a/game/server/mapzones/models.py +++ b/game/server/mapzones/models.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from pydantic import BaseModel from game.server.leaflet import LeafletPoint, LeafletPoly, ShapelyUtil -from game.theater import ConflictTheater +from game.theater import ConflictTheater, Player from game.threatzones import ThreatZones if TYPE_CHECKING: @@ -85,9 +85,9 @@ class ThreatZoneContainerJs(BaseModel): def for_game(game: Game) -> ThreatZoneContainerJs: return ThreatZoneContainerJs( blue=ThreatZonesJs.from_zones( - game.threat_zone_for(player=True), game.theater + game.threat_zone_for(player=Player.BLUE), game.theater ), red=ThreatZonesJs.from_zones( - game.threat_zone_for(player=False), game.theater + game.threat_zone_for(player=Player.RED), game.theater ), ) diff --git a/game/server/navmesh/routes.py b/game/server/navmesh/routes.py index 01b390e1..8a5ddce4 100644 --- a/game/server/navmesh/routes.py +++ b/game/server/navmesh/routes.py @@ -2,12 +2,13 @@ from fastapi import APIRouter, Depends from game import Game from game.server import GameContext +from game.theater.player import Player from .models import NavMeshJs router: APIRouter = APIRouter(prefix="/navmesh") @router.get("/", operation_id="get_navmesh", response_model=NavMeshJs) -def get(for_player: bool, game: Game = Depends(GameContext.require)) -> NavMeshJs: +def get(for_player: Player, game: Game = Depends(GameContext.require)) -> NavMeshJs: mesh = game.coalition_for(for_player).nav_mesh return NavMeshJs.from_navmesh(mesh, game) diff --git a/game/server/supplyroutes/models.py b/game/server/supplyroutes/models.py index cc1dffed..462b2f9c 100644 --- a/game/server/supplyroutes/models.py +++ b/game/server/supplyroutes/models.py @@ -76,6 +76,10 @@ class SupplyRouteJs(BaseModel): def for_link( game: Game, a: ControlPoint, b: ControlPoint, points: list[Point], sea: bool ) -> SupplyRouteJs: + if a.captured.is_blue: + blue = True + else: + blue = False return SupplyRouteJs( # Although these are not persistent objects in the backend, the frontend # needs unique IDs for anything that it will use in a list. That means that @@ -93,7 +97,7 @@ class SupplyRouteJs(BaseModel): points=[p.latlng() for p in points], front_active=not sea and a.front_is_active(b), is_sea=sea, - blue=a.captured, + blue=blue, active_transports=TransportFinder(game, a, b).describe_active_transports( sea ), diff --git a/game/server/tgos/models.py b/game/server/tgos/models.py index 28fecc53..2d03ba62 100644 --- a/game/server/tgos/models.py +++ b/game/server/tgos/models.py @@ -34,12 +34,16 @@ class TgoJs(BaseModel): def for_tgo(tgo: TheaterGroundObject) -> TgoJs: threat_ranges = [group.max_threat_range().meters for group in tgo.groups] detection_ranges = [group.max_detection_range().meters for group in tgo.groups] + if tgo.control_point.captured.is_blue: + blue = True + else: + blue = False return TgoJs( id=tgo.id, name=tgo.name, control_point_name=tgo.control_point.name, category=tgo.category, - blue=tgo.control_point.captured, + blue=blue, position=tgo.position.latlng(), units=[unit.display_name for unit in tgo.units], threat_ranges=threat_ranges, diff --git a/game/sim/combat/aircombat.py b/game/sim/combat/aircombat.py index 0fc6e948..33f8b461 100644 --- a/game/sim/combat/aircombat.py +++ b/game/sim/combat/aircombat.py @@ -46,7 +46,7 @@ class AirCombat(JoinableCombat): blue_flights = [] red_flights = [] for flight in self.flights: - if flight.squadron.player: + if flight.squadron.player.is_blue: blue_flights.append(str(flight)) else: red_flights.append(str(flight)) @@ -71,7 +71,7 @@ class AirCombat(JoinableCombat): blue = [] red = [] for flight in self.flights: - if flight.squadron.player: + if flight.squadron.player.is_blue: blue.append(flight) else: red.append(flight) diff --git a/game/sim/combat/combatinitiator.py b/game/sim/combat/combatinitiator.py index c5d064d9..f5309b1e 100644 --- a/game/sim/combat/combatinitiator.py +++ b/game/sim/combat/combatinitiator.py @@ -6,6 +6,7 @@ from collections.abc import Iterator from datetime import timedelta from typing import Optional, TYPE_CHECKING +from game.theater.player import Player from .aircombat import AirCombat from .aircraftengagementzones import AircraftEngagementZones from .atip import AtIp @@ -31,8 +32,10 @@ class CombatInitiator: def update_active_combats(self) -> None: blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato) red_a2a = AircraftEngagementZones.from_ato(self.game.red.ato) - blue_sam = SamEngagementZones.from_theater(self.game.theater, player=True) - red_sam = SamEngagementZones.from_theater(self.game.theater, player=False) + blue_sam = SamEngagementZones.from_theater( + self.game.theater, player=Player.BLUE + ) + red_sam = SamEngagementZones.from_theater(self.game.theater, player=Player.RED) # Check each vulnerable flight to see if it has initiated combat. If any flight # initiates combat, a single FrozenCombat will be created for all involved @@ -46,7 +49,7 @@ class CombatInitiator: if flight.state.in_combat: return - if flight.squadron.player: + if flight.squadron.player.is_blue: a2a = red_a2a own_a2a = blue_a2a sam = red_sam diff --git a/game/sim/combat/samengagementzones.py b/game/sim/combat/samengagementzones.py index fcaeefa3..77034490 100644 --- a/game/sim/combat/samengagementzones.py +++ b/game/sim/combat/samengagementzones.py @@ -9,7 +9,7 @@ from shapely.ops import unary_union from game.utils import dcs_to_shapely_point if TYPE_CHECKING: - from game.theater import ConflictTheater, TheaterGroundObject + from game.theater import ConflictTheater, TheaterGroundObject, Player from game.threatzones import ThreatPoly @@ -31,7 +31,9 @@ class SamEngagementZones: yield tgo @classmethod - def from_theater(cls, theater: ConflictTheater, player: bool) -> SamEngagementZones: + def from_theater( + cls, theater: ConflictTheater, player: Player + ) -> SamEngagementZones: commit_regions = [] individual_zones = [] for cp in theater.control_points_for(player): diff --git a/game/sim/gameupdateevents.py b/game/sim/gameupdateevents.py index 913b5963..56564393 100644 --- a/game/sim/gameupdateevents.py +++ b/game/sim/gameupdateevents.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from game.ato import Flight, Package from game.navmesh import NavMesh from game.sim.combat import FrozenCombat - from game.theater import ControlPoint, FrontLine, TheaterGroundObject + from game.theater import ControlPoint, FrontLine, TheaterGroundObject, Player from game.threatzones import ThreatZones from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode @@ -24,9 +24,9 @@ class GameUpdateEvents: updated_combats: list[FrozenCombat] = field(default_factory=list) ended_combats: list[FrozenCombat] = field(default_factory=list) updated_flight_positions: list[tuple[Flight, Point]] = field(default_factory=list) - navmesh_updates: dict[bool, NavMesh] = field(default_factory=dict) + navmesh_updates: dict[Player, NavMesh] = field(default_factory=dict) unculled_zones_updated: list[Point] = field(default_factory=list) - threat_zones_updated: dict[bool, ThreatZones] = field(default_factory=dict) + threat_zones_updated: dict[Player, ThreatZones] = field(default_factory=dict) new_flights: set[Flight] = field(default_factory=set) updated_flights: set[Flight] = field(default_factory=set) deleted_flights: set[UUID] = field(default_factory=set) @@ -70,7 +70,7 @@ class GameUpdateEvents: self.updated_flight_positions.append((flight, new_position)) return self - def update_navmesh(self, player: bool, navmesh: NavMesh) -> GameUpdateEvents: + def update_navmesh(self, player: Player, navmesh: NavMesh) -> GameUpdateEvents: self.navmesh_updates[player] = navmesh return self @@ -78,7 +78,9 @@ class GameUpdateEvents: self.unculled_zones_updated = zones return self - def update_threat_zones(self, player: bool, zones: ThreatZones) -> GameUpdateEvents: + def update_threat_zones( + self, player: Player, zones: ThreatZones + ) -> GameUpdateEvents: self.threat_zones_updated[player] = zones return self diff --git a/game/sim/missionresultsprocessor.py b/game/sim/missionresultsprocessor.py index e0bb273d..3e704e0b 100644 --- a/game/sim/missionresultsprocessor.py +++ b/game/sim/missionresultsprocessor.py @@ -145,7 +145,7 @@ class MissionResultsProcessor: def commit_captures(self, debriefing: Debriefing, events: GameUpdateEvents) -> None: for captured in debriefing.base_captures: try: - if captured.captured_by_player: + if captured.captured_by_player.is_blue: self.game.message( f"{captured.control_point} captured!", f"We took control of {captured.control_point}.", diff --git a/game/squadrons/airwing.py b/game/squadrons/airwing.py index 1e4e7016..e9df8674 100644 --- a/game/squadrons/airwing.py +++ b/game/squadrons/airwing.py @@ -14,12 +14,13 @@ from ..utils import Distance if TYPE_CHECKING: from game.game import Game + from game.theater.player import Player from ..ato.flighttype import FlightType from .squadron import Squadron class AirWing: - def __init__(self, player: bool, game: Game, faction: Faction) -> None: + def __init__(self, player: Player, game: Game, faction: Faction) -> None: self.player = player self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) self.squadron_defs = SquadronDefLoader(game, faction).load() diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 7ef9235d..65b28ccf 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: from game import Game from game.coalition import Coalition from game.dcs.aircrafttype import AircraftType - from game.theater import ControlPoint, MissionTarget + from game.theater import ControlPoint, MissionTarget, Player from .operatingbases import OperatingBases from .squadrondef import SquadronDef @@ -96,7 +96,7 @@ class Squadron: self._livery_pool: list[str] = [] @property - def player(self) -> bool: + def player(self) -> Player: return self.coalition.player def assign_to_base(self, base: ControlPoint) -> None: @@ -134,7 +134,7 @@ class Squadron: return self.claim_new_pilot_if_allowed() # For opfor, so player/AI option is irrelevant. - if not self.player: + if self.player != Player.BLUE: return self.available_pilots.pop() preference = self.settings.auto_ato_behavior diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 17d55a5c..1c857db0 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -15,6 +15,7 @@ from .daytimemap import DaytimeMap from .frontline import FrontLine from .iadsnetwork.iadsnetwork import IadsNetwork from .landmap import poly_contains, load_landmap +from .player import Player from .seasonalconditions import SeasonalConditions from ..utils import Heading @@ -170,10 +171,10 @@ class ConflictTheater: return new_point def control_points_for( - self, player: bool, state_check: bool = False + self, player: Player, state_check: bool = False ) -> Iterator[ControlPoint]: for point in self.controlpoints: - if point.captured == player: + if point.captured is player: if not state_check: yield point elif point.is_carrier and point.runway_is_operational(): @@ -182,14 +183,21 @@ class ConflictTheater: yield point def player_points(self, state_check: bool = False) -> List[ControlPoint]: - return list(self.control_points_for(player=True, state_check=state_check)) + return list( + self.control_points_for(player=Player.BLUE, state_check=state_check) + ) def conflicts(self) -> Iterator[FrontLine]: for cp in self.player_points(): yield from cp.front_lines.values() def enemy_points(self, state_check: bool = False) -> List[ControlPoint]: - return list(self.control_points_for(player=False, state_check=state_check)) + return list(self.control_points_for(player=Player.RED, state_check=state_check)) + + def neutral_points(self, state_check: bool = False) -> List[ControlPoint]: + return list( + self.control_points_for(player=Player.NEUTRAL, state_check=state_check) + ) def closest_control_point( self, point: Point, allow_naval: bool = False @@ -259,10 +267,12 @@ class ConflictTheater: """ closest_cps = list() distances_to_cp = dict() - if cp.captured: + if cp.captured.is_blue: control_points = self.player_points() - else: + elif cp.captured.is_red: control_points = self.enemy_points() + elif cp.captured.is_neutral: + control_points = self.neutral_points() for other_cp in control_points: if cp == other_cp: continue diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 347587c5..e858cf62 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -68,6 +68,7 @@ from .base import Base from .frontline import FrontLine from .interfaces.CTLD import CTLD from .missiontarget import MissionTarget +from .player import Player from .theatergroundobject import ( GenericCarrierGroundObject, TheaterGroundObject, @@ -377,7 +378,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): position: Point, at: StartingPosition, theater: ConflictTheater, - starts_blue: bool, + starting_coalition: Player, cptype: ControlPointType = ControlPointType.AIRBASE, is_invisible: bool = False, ) -> None: @@ -386,8 +387,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): self.full_name = name self.at = at self.theater = theater - self.starts_blue = starts_blue self.is_invisible = is_invisible + self.starting_coalition = starting_coalition self.connected_objectives: List[TheaterGroundObject] = [] self.preset_locations = PresetLocations() self.helipads: List[PointWithHeading] = [] @@ -435,7 +436,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): def finish_init(self, game: Game) -> None: assert self._coalition is None - self._coalition = game.coalition_for(self.starts_blue) + self._coalition = game.coalition_for(self.starting_coalition) assert self._front_line_db is None self._front_line_db = game.db.front_lines @@ -444,7 +445,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): # the entire game state when it comes up. from game.sim import GameUpdateEvents - self._create_missing_front_lines(laser_code_registry, GameUpdateEvents()) + if self.captured != Player.NEUTRAL: + self._create_missing_front_lines(laser_code_registry, GameUpdateEvents()) @property def front_line_db(self) -> Database[FrontLine]: @@ -455,9 +457,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): self, laser_code_registry: LaserCodeRegistry, events: GameUpdateEvents ) -> None: for connection in self.convoy_routes.keys(): - if not connection.front_line_active_with( - self - ) and not connection.is_friendly_to(self): + if ( + not connection.front_line_active_with(self) + and not connection.is_friendly_to(self) + and connection.captured != Player.NEUTRAL + ): self._create_front_line_with(laser_code_registry, connection, events) def _create_front_line_with( @@ -491,6 +495,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): @property def has_frontline(self) -> bool: + if self.captured.is_neutral: + return False return bool(self.front_lines) def front_line_active_with(self, other: ControlPoint) -> bool: @@ -500,14 +506,17 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): return self.front_lines[other] @property - def captured(self) -> bool: + def captured(self) -> Player: return self.coalition.player @property def standard_identity(self) -> StandardIdentity: - return ( - StandardIdentity.FRIEND if self.captured else StandardIdentity.HOSTILE_FAKER - ) + if self.captured.is_neutral: + return StandardIdentity.UNKNOWN + elif self.captured.is_blue: + return StandardIdentity.FRIEND + else: + return StandardIdentity.HOSTILE_FAKER @property def sidc_status(self) -> Status: @@ -819,7 +828,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): found.append(g) return found - def is_friendly(self, to_player: bool) -> bool: + def is_friendly(self, to_player: Player) -> bool: return self.captured == to_player def is_friendly_to(self, control_point: ControlPoint) -> bool: @@ -828,7 +837,9 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): def capture_equipment(self, game: Game) -> None: total = self.base.total_armor_value self.base.armor.clear() - game.adjust_budget(total, player=not self.captured) + game.adjust_budget( + total, player=Player.BLUE if self.captured.is_red else Player.RED + ) game.message( f"{self.name} is not connected to any friendly points. Ground " f"vehicles have been captured and sold for ${total}M." @@ -857,7 +868,9 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): def capture_aircraft(self, game: Game, airframe: AircraftType, count: int) -> None: value = airframe.price * count - game.adjust_budget(value, player=not self.captured) + game.adjust_budget( + value, player=Player.BLUE if self.captured.is_red else Player.RED + ) game.message( f"No valid retreat destination in range of {self.name} for {airframe} " f"{count} aircraft have been captured and sold for ${value}M." @@ -960,7 +973,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): pass # TODO: Should be Airbase specific. - def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: + def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None: new_coalition = game.coalition_for(for_player) self.ground_unit_orders.refund_all(self.coalition) self.retreat_ground_units(game) @@ -1125,6 +1138,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): @property def has_active_frontline(self) -> bool: + if self.captured.is_neutral: + return False return any(not c.is_friendly(self.captured) for c in self.connected_points) def front_is_active(self, other: ControlPoint) -> bool: @@ -1211,7 +1226,7 @@ class Airfield(ControlPoint, CTLD): self, airport: Airport, theater: ConflictTheater, - starts_blue: bool, + starting_coalition: Player, ctld_zones: Optional[List[Tuple[Point, float]]] = None, influence_zone: Optional[List[Tuple[Point, float]]] = None, ) -> None: @@ -1220,7 +1235,7 @@ class Airfield(ControlPoint, CTLD): airport.position, airport, theater, - starts_blue, + starting_coalition, cptype=ControlPointType.AIRBASE, ) self.airport = airport @@ -1252,7 +1267,7 @@ class Airfield(ControlPoint, CTLD): return True return self.runway_is_operational() - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if not self.is_friendly(for_player): @@ -1373,7 +1388,7 @@ class NavalControlPoint( def is_fleet(self) -> bool: return True - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if self.is_friendly(for_player): @@ -1482,7 +1497,7 @@ class NavalControlPoint( class Carrier(NavalControlPoint): def __init__( - self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool + self, name: str, at: Point, theater: ConflictTheater, starts_blue: Player ): super().__init__( name, @@ -1497,7 +1512,7 @@ class Carrier(NavalControlPoint): def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER - def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: + def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None: raise RuntimeError("Carriers cannot be captured") @property @@ -1522,7 +1537,7 @@ class EssexCarrier(Carrier): class Lha(NavalControlPoint): def __init__( - self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool + self, name: str, at: Point, theater: ConflictTheater, starts_blue: Player ): super().__init__( name, at, at, theater, starts_blue, cptype=ControlPointType.LHA_GROUP @@ -1532,7 +1547,7 @@ class Lha(NavalControlPoint): def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.AMPHIBIOUS_ASSAULT_SHIP_GENERAL - def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: + def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None: raise RuntimeError("LHAs cannot be captured") @property @@ -1555,7 +1570,7 @@ class OffMapSpawn(ControlPoint): return True def __init__( - self, name: str, position: Point, theater: ConflictTheater, starts_blue: bool + self, name: str, position: Point, theater: ConflictTheater, starts_blue: Player ): super().__init__( name, @@ -1570,10 +1585,10 @@ class OffMapSpawn(ControlPoint): def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.LAND_INSTALLATIONS, LandInstallationEntity.AIPORT_AIR_BASE - def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: + def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None: raise RuntimeError("Off map control points cannot be captured") - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: yield from [] def total_aircraft_parking(self, parking_type: ParkingType) -> int: @@ -1630,7 +1645,7 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD): name: str, at: Point, theater: ConflictTheater, - starts_blue: bool, + starts_blue: Player, ctld_zones: Optional[List[Tuple[Point, float]]] = None, is_invisible: bool = False, influence_zone: Optional[List[Tuple[Point, float]]] = None, @@ -1667,7 +1682,7 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD): def runway_status(self) -> RunwayStatus: return RunwayStatus() - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if not self.is_friendly(for_player): diff --git a/game/theater/frontline.py b/game/theater/frontline.py index 47a0b88d..3c510ee0 100644 --- a/game/theater/frontline.py +++ b/game/theater/frontline.py @@ -8,6 +8,7 @@ from typing import Any, Iterator, List, TYPE_CHECKING, Tuple from dcs.mapping import Point from .missiontarget import MissionTarget +from .player import Player from ..lasercodes.lasercode import LaserCode from ..utils import Heading, pairwise @@ -89,19 +90,22 @@ class FrontLine(MissionTarget): def update_position(self) -> None: self.position = self._compute_position() - def control_point_friendly_to(self, player: bool) -> ControlPoint: - if player: + def control_point_friendly_to(self, player: Player) -> ControlPoint: + if player.is_blue: return self.blue_cp return self.red_cp - def control_point_hostile_to(self, player: bool) -> ControlPoint: - return self.control_point_friendly_to(not player) + def control_point_hostile_to(self, player: Player) -> ControlPoint: + if player.is_blue: + return self.red_cp + else: + return self.blue_cp - def is_friendly(self, to_player: bool) -> bool: + def is_friendly(self, to_player: Player) -> bool: """Returns True if the objective is in friendly territory.""" return False - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType yield from [ @@ -210,6 +214,6 @@ class FrontLine(MissionTarget): raise ValueError( "Cannot sort control points that are friendly to each other" ) - if a.captured: + if a.captured.is_blue: return a, b return b, a diff --git a/game/theater/iadsnetwork/iadsnetwork.py b/game/theater/iadsnetwork/iadsnetwork.py index 21e5f663..d62e0aca 100644 --- a/game/theater/iadsnetwork/iadsnetwork.py +++ b/game/theater/iadsnetwork/iadsnetwork.py @@ -16,6 +16,7 @@ from game.theater.theatergroundobject import ( TheaterGroundObject, ) from game.theater.theatergroup import IadsGroundGroup +from game.theater.player import Player if TYPE_CHECKING: from game.game import Game @@ -31,7 +32,7 @@ class SkynetNode: """Dataclass for a SkynetNode used in the LUA Data table by the luagenerator""" dcs_name: str - player: bool + player: Player iads_role: IadsRole properties: dict[str, str] = field(default_factory=dict) connections: dict[str, list[str]] = field(default_factory=lambda: defaultdict(list)) @@ -62,7 +63,7 @@ class SkynetNode: def from_group(cls, group: IadsGroundGroup) -> SkynetNode: node = cls( cls.dcs_name_for_group(group), - group.ground_object.is_friendly(True), + group.ground_object.coalition.player, group.iads_role, ) unit_type = group.units[0].unit_type @@ -314,8 +315,8 @@ class IadsNetwork: self._make_advanced_connections_by_range(node) def _is_friendly(self, node: IadsNetworkNode, tgo: TheaterGroundObject) -> bool: - node_friendly = node.group.ground_object.is_friendly(True) - tgo_friendly = tgo.is_friendly(True) + node_friendly = node.group.ground_object.is_friendly(Player.BLUE) + tgo_friendly = tgo.is_friendly(Player.BLUE) return node_friendly == tgo_friendly def _update_network( diff --git a/game/theater/missiontarget.py b/game/theater/missiontarget.py index 4637aa44..d98b2cc7 100644 --- a/game/theater/missiontarget.py +++ b/game/theater/missiontarget.py @@ -6,7 +6,7 @@ from dcs.mapping import Point if TYPE_CHECKING: from game.ato.flighttype import FlightType - from game.theater import TheaterUnit, Coalition + from game.theater import TheaterUnit, Coalition, Player class MissionTarget: @@ -24,11 +24,11 @@ class MissionTarget: """Computes the distance to the given mission target.""" return self.position.distance_to_point(other.position) - def is_friendly(self, to_player: bool) -> bool: + def is_friendly(self, to_player: Player) -> bool: """Returns True if the objective is in friendly territory.""" raise NotImplementedError - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if self.is_friendly(for_player): diff --git a/game/theater/player.py b/game/theater/player.py new file mode 100644 index 00000000..9464e02d --- /dev/null +++ b/game/theater/player.py @@ -0,0 +1,30 @@ +from enum import Enum + + +class Player(Enum): + NEUTRAL = "Neutral" + BLUE = "Blue" + RED = "Red" + + @property + def is_red(self) -> bool: + """Returns True if the player is Red.""" + return self == Player.RED + + @property + def is_blue(self) -> bool: + """Returns True if the player is Blue.""" + return self == Player.BLUE + + @property + def is_neutral(self) -> bool: + """Returns True if the player is Neutral.""" + return self == Player.NEUTRAL + + @property + def opponent(self) -> "Player": + """Returns the opponent player.""" + if self.is_blue: + return Player.RED + else: + return Player.BLUE diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index d7525a9e..41e65899 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -32,6 +32,7 @@ from . import ( Fob, OffMapSpawn, ) +from .player import Player from .theatergroup import ( IadsGroundGroup, IadsRole, @@ -158,12 +159,12 @@ class GameGenerator: game.settings.version = VERSION return game - def should_remove_carrier(self, player: bool) -> bool: - faction = self.player if player else self.enemy + def should_remove_carrier(self, player: Player) -> bool: + faction = self.player if player.is_blue else self.enemy return self.generator_settings.no_carrier or not faction.carriers - def should_remove_lha(self, player: bool) -> bool: - faction = self.player if player else self.enemy + def should_remove_lha(self, player: Player) -> bool: + faction = self.player if player.is_blue else self.enemy return self.generator_settings.no_lha or not [ x for x in faction.carriers if x.unit_class == UnitClass.HELICOPTER_CARRIER ] @@ -174,11 +175,13 @@ class GameGenerator: # Remove carrier and lha, invert situation if needed for cp in self.theater.controlpoints: if self.generator_settings.inverted: - cp.starts_blue = cp.captured_invert + cp.starting_coalition = ( + Player.RED if not cp.captured_invert else Player.BLUE + ) - if cp.is_carrier and self.should_remove_carrier(cp.starts_blue): + if cp.is_carrier and self.should_remove_carrier(cp.starting_coalition): to_remove.append(cp) - elif cp.is_lha and self.should_remove_lha(cp.starts_blue): + elif cp.is_lha and self.should_remove_lha(cp.starting_coalition): to_remove.append(cp) # do remove @@ -230,11 +233,13 @@ class ControlPointGroundObjectGenerator: self.control_point.connected_objectives.append(ground_object) def generate_navy(self) -> None: + if self.control_point.captured.is_neutral: + return skip_player_navy = self.generator_settings.no_player_navy - if self.control_point.captured and skip_player_navy: + if self.control_point.captured.is_blue and skip_player_navy: return skip_enemy_navy = self.generator_settings.no_enemy_navy - if not self.control_point.captured and skip_enemy_navy: + if self.control_point.captured.is_red and skip_enemy_navy: return for position in self.control_point.preset_locations.ships: unit_group = self.armed_forces.random_group_for_task(GroupTask.NAVY) @@ -352,7 +357,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator): self.control_point.name, self.control_point.position, self.game.theater, - self.control_point.starts_blue, + self.control_point.starting_coalition, ) self.control_point.finish_init(self.game) self.game.theater.controlpoints.append(self.control_point) diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index d4663cca..7bdb7fe8 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -21,6 +21,7 @@ from game.sidc import ( ) from game.theater.presetlocation import PresetLocation from .missiontarget import MissionTarget +from .player import Player from ..data.groups import GroupTask from ..utils import Distance, Heading, meters @@ -98,11 +99,12 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC): @property def standard_identity(self) -> StandardIdentity: - return ( - StandardIdentity.FRIEND - if self.control_point.captured - else StandardIdentity.HOSTILE_FAKER - ) + if self.control_point.captured.is_blue: + return StandardIdentity.FRIEND + elif self.control_point.captured.is_neutral: + return StandardIdentity.UNKNOWN + else: + return StandardIdentity.HOSTILE_FAKER @property def is_dead(self) -> bool: @@ -154,10 +156,12 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC): def faction_color(self) -> str: return "BLUE" if self.control_point.captured else "RED" - def is_friendly(self, to_player: bool) -> bool: + def is_friendly(self, to_player: Player) -> bool: + if self.control_point.captured.is_neutral: + return False return self.control_point.is_friendly(to_player) - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if self.is_friendly(for_player): @@ -360,7 +364,7 @@ class BuildingGroundObject(TheaterGroundObject): class NavalGroundObject(TheaterGroundObject, ABC): - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if not self.is_friendly(for_player): @@ -466,7 +470,7 @@ class MissileSiteGroundObject(TheaterGroundObject): def should_head_to_conflict(self) -> bool: return True - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if not self.is_friendly(for_player): @@ -507,7 +511,7 @@ class CoastalSiteGroundObject(TheaterGroundObject): def should_head_to_conflict(self) -> bool: return True - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if not self.is_friendly(for_player): @@ -534,7 +538,7 @@ class IadsGroundObject(TheaterGroundObject, ABC): task=task, ) - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if not self.is_friendly(for_player): @@ -585,7 +589,7 @@ class SamGroundObject(IadsGroundObject): def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.LAND_UNIT, LandUnitEntity.AIR_DEFENSE - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if not self.is_friendly(for_player): @@ -642,7 +646,7 @@ class VehicleGroupGroundObject(TheaterGroundObject): def should_head_to_conflict(self) -> bool: return True - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if not self.is_friendly(for_player): @@ -697,7 +701,7 @@ class ShipGroundObject(NavalGroundObject): class IadsBuildingGroundObject(BuildingGroundObject): - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: from game.ato import FlightType if not self.is_friendly(for_player): diff --git a/game/theater/transitnetwork.py b/game/theater/transitnetwork.py index 49694c9a..8d2da2bb 100644 --- a/game/theater/transitnetwork.py +++ b/game/theater/transitnetwork.py @@ -9,6 +9,7 @@ from typing import Dict, Iterator, List, Optional, Set, Tuple from .conflicttheater import ConflictTheater from .controlpoint import ControlPoint +from .player import Player class NoPathError(RuntimeError): @@ -152,7 +153,7 @@ class TransitNetwork: class TransitNetworkBuilder: - def __init__(self, theater: ConflictTheater, for_player: bool) -> None: + def __init__(self, theater: ConflictTheater, for_player: Player) -> None: self.control_points = list(theater.control_points_for(for_player)) self.network = TransitNetwork() self.airports: Set[ControlPoint] = { diff --git a/game/threatzones.py b/game/threatzones.py index 07fec2bd..7bc67457 100644 --- a/game/threatzones.py +++ b/game/threatzones.py @@ -26,6 +26,7 @@ from game.utils import Distance, meters, nautical_miles if TYPE_CHECKING: from game import Game + from game.theater.player import Player ThreatPoly = Union[MultiPolygon, Polygon] @@ -187,7 +188,7 @@ class ThreatZones: return min(cap_threat_range, max_distance) @classmethod - def for_faction(cls, game: Game, player: bool) -> ThreatZones: + def for_faction(cls, game: Game, player: Player) -> ThreatZones: """Generates the threat zones projected by the given coalition. Args: diff --git a/game/transfers.py b/game/transfers.py index e498d16b..169f4d58 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -50,7 +50,14 @@ from game.dcs.aircrafttype import AircraftType from game.dcs.groundunittype import GroundUnitType from game.naming import namegen from game.procurement import AircraftProcurementRequest -from game.theater import ControlPoint, MissionTarget, ParkingType, Carrier, Airfield +from game.theater import ( + ControlPoint, + MissionTarget, + ParkingType, + Carrier, + Airfield, + Player, +) from game.theater.transitnetwork import ( TransitConnection, TransitNetwork, @@ -92,7 +99,7 @@ class TransferOrder: position: ControlPoint = field(init=False) #: True if the transfer order belongs to the player. - player: bool = field(init=False) + player: Player = field(init=False) #: The units being transferred. units: dict[GroundUnitType, int] @@ -111,7 +118,7 @@ class TransferOrder: def __post_init__(self) -> None: self.position = self.origin - self.player = self.origin.is_friendly(to_player=True) + self.player = self.origin.captured @property def description(self) -> str: @@ -239,7 +246,10 @@ class Airlift(Transport): @property def player_owned(self) -> bool: - return self.transfer.player + if self.transfer.player.is_blue: + return True + else: + return False def find_escape_route(self) -> Optional[ControlPoint]: # TODO: Move units to closest base. @@ -384,8 +394,11 @@ class MultiGroupTransport(MissionTarget, Transport): self.origin = origin self.transfers: List[TransferOrder] = [] - def is_friendly(self, to_player: bool) -> bool: - return self.origin.captured + def is_friendly(self, to_player: Player) -> bool: + if self.origin.captured.is_blue: + return True + else: + return False def add_units(self, transfer: TransferOrder) -> None: self.transfers.append(transfer) @@ -432,7 +445,7 @@ class MultiGroupTransport(MissionTarget, Transport): yield unit_type @property - def player_owned(self) -> bool: + def player_owned(self) -> Player: return self.origin.captured def find_escape_route(self) -> Optional[ControlPoint]: @@ -450,7 +463,7 @@ class Convoy(MultiGroupTransport): def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None: super().__init__(namegen.next_convoy_name(), origin, destination) - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: if self.is_friendly(for_player): return @@ -476,7 +489,7 @@ class CargoShip(MultiGroupTransport): def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None: super().__init__(namegen.next_cargo_ship_name(), origin, destination) - def mission_types(self, for_player: bool) -> Iterator[FlightType]: + def mission_types(self, for_player: Player) -> Iterator[FlightType]: if self.is_friendly(for_player): return @@ -570,7 +583,7 @@ class CargoShipMap(TransportMap[CargoShip]): class PendingTransfers: - def __init__(self, game: Game, player: bool) -> None: + def __init__(self, game: Game, player: Player) -> None: self.game = game self.player = player self.convoys = ConvoyMap() diff --git a/qt_ui/models.py b/qt_ui/models.py index 9035eaed..b1fd4181 100644 --- a/qt_ui/models.py +++ b/qt_ui/models.py @@ -24,7 +24,7 @@ from game.radio.tacan import TacanChannel from game.server import EventStream from game.sim.gameupdateevents import GameUpdateEvents from game.squadrons.squadron import Pilot, Squadron -from game.theater import NavalControlPoint +from game.theater import NavalControlPoint, Player from game.theater.missiontarget import MissionTarget from game.transfers import PendingTransfers, TransferOrder from qt_ui.simcontroller import SimController @@ -564,8 +564,8 @@ class GameModel: self.allocated_icls: list[int] = list() self.init_comms_registry() - def ato_model_for(self, player: bool) -> AtoModel: - if player: + def ato_model_for(self, player: Player) -> AtoModel: + if player.is_blue: return self.ato_model return self.red_ato_model diff --git a/qt_ui/widgets/QIntelBox.py b/qt_ui/widgets/QIntelBox.py index 5e3deaf5..4030c765 100644 --- a/qt_ui/widgets/QIntelBox.py +++ b/qt_ui/widgets/QIntelBox.py @@ -10,6 +10,7 @@ from PySide6.QtWidgets import ( from game import Game from game.income import Income +from game.theater import Player from qt_ui.windows.intel import IntelWindow @@ -76,8 +77,8 @@ class QIntelBox(QGroupBox): def economic_strength_text(self) -> str: assert self.game is not None - own = Income(self.game, player=True).total - enemy = Income(self.game, player=False).total + own = Income(self.game, player=Player.BLUE).total + enemy = Income(self.game, player=Player.RED).total if not enemy: return "enemy economy ruined" diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index 2c02d947..7eaa6762 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -6,6 +6,7 @@ from game.ato.flightwaypointtype import FlightWaypointType from game.missiongenerator.frontlineconflictdescription import ( FrontLineConflictDescription, ) +from game.theater.player import Player from game.theater.controlpoint import ControlPointType from game.utils import Distance from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox @@ -93,7 +94,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): wpt.targets.append(target) wpt.obj_name = tgo.obj_name wpt.waypoint_type = FlightWaypointType.CUSTOM - if tgo.is_friendly(to_player=True): + if tgo.is_friendly(to_player=Player.BLUE): wpt.description = f"Friendly unit: {target.name}" else: wpt.description = f"Enemy unit: {target.name}" diff --git a/qt_ui/windows/AirWingConfigurationDialog.py b/qt_ui/windows/AirWingConfigurationDialog.py index fec17c79..1116527b 100644 --- a/qt_ui/windows/AirWingConfigurationDialog.py +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -790,7 +790,7 @@ class AirWingConfigurationDialog(QDialog): self.tabs = [] for coalition in game.coalitions: coalition_tab = AirWingConfigurationTab(coalition, game, aircraft_present) - name = "Blue" if coalition.player else "Red" + name = "Blue" if coalition.player.is_blue else "Red" self.tab_widget.addTab(coalition_tab, name) self.tabs.append(coalition_tab) diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py index 779eb1bd..db3940f2 100644 --- a/qt_ui/windows/QDebriefingWindow.py +++ b/qt_ui/windows/QDebriefingWindow.py @@ -14,13 +14,14 @@ from PySide6.QtWidgets import ( ) from game.debriefing import Debriefing +from game.theater import Player from qt_ui.windows.GameUpdateSignal import GameUpdateSignal T = TypeVar("T") class LossGrid(QGridLayout): - def __init__(self, debriefing: Debriefing, player: bool) -> None: + def __init__(self, debriefing: Debriefing, player: Player) -> None: super().__init__() self.add_loss_rows( @@ -57,8 +58,10 @@ class LossGrid(QGridLayout): class ScrollingCasualtyReportContainer(QGroupBox): - def __init__(self, debriefing: Debriefing, player: bool) -> None: - country = debriefing.player_country if player else debriefing.enemy_country + def __init__(self, debriefing: Debriefing, player: Player) -> None: + country = ( + debriefing.player_country if player.is_blue else debriefing.enemy_country + ) super().__init__(f"{country}'s lost units:") scroll_content = QWidget() scroll_content.setLayout(LossGrid(debriefing, player)) @@ -91,10 +94,14 @@ class QDebriefingWindow(QDialog): title = QLabel("Casualty report") layout.addWidget(title) - player_lost_units = ScrollingCasualtyReportContainer(debriefing, player=True) + player_lost_units = ScrollingCasualtyReportContainer( + debriefing, player=Player.BLUE + ) layout.addWidget(player_lost_units) - enemy_lost_units = ScrollingCasualtyReportContainer(debriefing, player=False) + enemy_lost_units = ScrollingCasualtyReportContainer( + debriefing, player=Player.RED + ) layout.addWidget(enemy_lost_units, 1) okay = QPushButton("Okay") diff --git a/qt_ui/windows/basemenu/NewUnitTransferDialog.py b/qt_ui/windows/basemenu/NewUnitTransferDialog.py index dbe8dbab..cda88618 100644 --- a/qt_ui/windows/basemenu/NewUnitTransferDialog.py +++ b/qt_ui/windows/basemenu/NewUnitTransferDialog.py @@ -25,7 +25,7 @@ from dcs.unittype import UnitType from game import Game from game.dcs.groundunittype import GroundUnitType -from game.theater import ControlPoint +from game.theater import ControlPoint, Player from game.transfers import TransferOrder from qt_ui.models import GameModel from qt_ui.widgets.QLabeledWidget import QLabeledWidget @@ -40,7 +40,7 @@ class TransferDestinationComboBox(QComboBox): for cp in self.game.theater.controlpoints: if ( cp != self.origin - and cp.is_friendly(to_player=True) + and cp.is_friendly(to_player=Player.BLUE) and cp.can_deploy_ground_units ): self.addItem(cp.name, cp) diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index 63a3b552..c322fbe1 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -27,6 +27,7 @@ from game.theater import ( FREE_FRONTLINE_UNIT_SUPPLY, NavalControlPoint, ParkingType, + Player, ) from qt_ui.dialogs import Dialog from qt_ui.models import GameModel @@ -85,7 +86,7 @@ class QBaseMenu2(QDialog): self.freq_widget = None self.link4_widget = None - is_friendly = cp.is_friendly(True) + is_friendly = cp.is_friendly(Player.BLUE) if is_friendly and isinstance(cp, RadioFrequencyContainer): self.freq_widget = QFrequencyWidget(cp, self.game_model) cp_settings.addWidget(self.freq_widget, counter // 2, counter % 2) diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index cbad2c9c..b2cdf683 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -19,7 +19,7 @@ from game.config import REWARDS from game.data.building_data import FORTIFICATION_BUILDINGS from game.server import EventStream from game.sim.gameupdateevents import GameUpdateEvents -from game.theater import ControlPoint, TheaterGroundObject +from game.theater import ControlPoint, TheaterGroundObject, Player from game.theater.theatergroundobject import ( BuildingGroundObject, ) @@ -87,12 +87,12 @@ class QGroundObjectMenu(QDialog): if isinstance(self.ground_object, BuildingGroundObject): self.mainLayout.addWidget(self.buildingBox) - if self.cp.captured: + if self.cp.captured.is_blue: self.mainLayout.addWidget(self.financesBox) else: self.mainLayout.addWidget(self.intelBox) self.mainLayout.addWidget(self.orientationBox) - if self.ground_object.is_iads and self.cp.is_friendly(to_player=False): + if self.ground_object.is_iads and self.cp.is_friendly(to_player=Player.RED): self.mainLayout.addWidget(self.hiddenBox) self.actionLayout = QHBoxLayout() @@ -118,8 +118,10 @@ class QGroundObjectMenu(QDialog): @property def show_buy_sell_actions(self) -> bool: + if self.cp.captured.is_neutral: + return False buysell_allowed = self.game.settings.enable_enemy_buy_sell - buysell_allowed |= self.cp.captured + buysell_allowed |= self.cp.captured.is_blue return buysell_allowed def doLayout(self): @@ -133,7 +135,7 @@ class QGroundObjectMenu(QDialog): QLabel(f"Unit {str(unit.display_name)}"), i, 0 ) - if not unit.alive and unit.repairable and self.cp.captured: + if not unit.alive and unit.repairable and self.cp.captured.is_blue: price = unit.unit_type.price if unit.unit_type else 0 repair = QPushButton(f"Repair [{price}M]") repair.setProperty("style", "btn-success") diff --git a/requirements.txt b/requirements.txt index 948a7098..3172378e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,11 @@ pluggy==1.5.0 pre-commit==4.2.0 pydantic==2.11.0b2 pydantic-settings==2.8.1 +<<<<<<< HEAD pydcs @ git+https://github.com/dcs-retribution/pydcs@60cddd944d454f59f5c866a2ec2ae3e91526da25 +======= +pydcs @ git+https://github.com/Druss99/pydcs@efbd67fc87de6efe40316523cdd96d1c91711f43 +>>>>>>> 9f9f53b26 (refactor of previous commits) pyinstaller==5.13.2 pyinstaller-hooks-contrib==2024.0 pyparsing==3.2.1 diff --git a/resources/campaigns/scenic_rout_neutral.yaml b/resources/campaigns/scenic_rout_neutral.yaml new file mode 100644 index 00000000..1f596233 --- /dev/null +++ b/resources/campaigns/scenic_rout_neutral.yaml @@ -0,0 +1,147 @@ +--- +name: Persian Gulf - Scenic Route - Neutral +theater: Persian Gulf +authors: Fuzzle +description:

A lightweight naval campaign involving a US Navy carrier group pushing across the coast of Iran. This is a purely naval campaign, meaning you will need to use the Air Assault mission type with transports to take the first FOB. Ensure you soften it up enough first!

Backstory: Iran has declared war on all US forces in the Gulf resulting in all local allies withdrawing their support for American troops. A lone carrier group must pacify the southern coast of Iran and hold out until backup can arrive lest the US and her interests be ejected from the region permanently.

+version: "10.7" +advanced_iads: true +recommended_player_faction: US Navy 2005 +recommended_enemy_faction: Iran 2015 +miz: scenic_route_neutral.miz +performance: 1 +recommended_start_date: 2005-04-26 +recommended_player_money: 1000 +recommended_enemy_money: 1300 +recommended_player_income_multiplier: 1.2 +recommended_enemy_income_multiplier: 0.7 +squadrons: + #BLUFOR CVN + Naval-1: + - primary: BARCAP + secondary: air-to-air + aircraft: + - VF-143 + size: 14 + - primary: SEAD + secondary: air-to-ground + aircraft: + - VFA-113 + - primary: AEW&C + aircraft: + - VAW-125 + size: 2 + - primary: Refueling + aircraft: + - VS-35 (Tanker) + size: 4 + - primary: Anti-ship + secondary: air-to-ground + aircraft: + - VS-35 + size: 8 + - primary: Transport + aircraft: + - HSM-40 + size: 2 + # BLUFOR LHA + Naval-2: + - primary: BAI + secondary: air-to-ground + aircraft: + - VMA-223 + size: 10 + - primary: Transport + secondary: air-to-ground + aircraft: + - HMLA-169 (UH-1H) + size: 4 + - primary: CAS + secondary: air-to-ground + aircraft: + - HMLA-169 (AH-1W) + size: 6 + # OPFOR CVN + Naval-3: + - primary: BARCAP + secondary: any + - primary: Strike + secondary: any + - primary: BAI + secondary: any + - primary: Refueling + # Kish Intl + 24: + - primary: AEW&C + aircraft: + - A-50 + size: 1 + - primary: BARCAP + secondary: any + aircraft: + - F-14A Tomcat (Block 135-GR Late) + - primary: Strike + secondary: air-to-ground + aircraft: + - Su-24MK Fencer-D + - primary: BARCAP + secondary: any + aircraft: + - F-4E Phantom II + # Havadarya + 9: + - primary: BARCAP + secondary: any + aircraft: + - F-4E Phantom II + size: 10 + - primary: CAS + secondary: air-to-ground + aircraft: + - Su-25 Frogfoot + size: 12 + # Bandar Abbas Intl + 2: + - primary: TARCAP + secondary: any + aircraft: + - F-5E Tiger II + - primary: BARCAP + secondary: air-to-ground + aircraft: + - MiG-29A Fulcrum-A + - primary: Strike + secondary: air-to-ground + aircraft: + - Su-24MK + - primary: BARCAP + secondary: air-to-ground + aircraft: + - F-4E Phantom II + # OPFOR First FOB + FOB Seerik: + - primary: CAS + secondary: air-to-ground + aircraft: + - Mi-24V Hind-E + size: 6 + # OPFOR Second FOB + FOB Kohnehshahr: + - primary: CAS + secondary: air-to-ground + aircraft: + - Mi-24V Hind-F + size: 6 + # OPFOR Third FOB + FOB Khvosh: + - primary: CAS + secondary: air-to-ground + aircraft: + - Mi-28N Havoc + size: 6 + # OPFOR Last FOB + FOB Charak: + - primary: CAS + secondary: air-to-ground + aircraft: + - Mi-28N Havoc + size: 6 \ No newline at end of file diff --git a/resources/campaigns/scenic_route_neutral.miz b/resources/campaigns/scenic_route_neutral.miz new file mode 100644 index 0000000000000000000000000000000000000000..66f1376b47d867d083a02a9686b537cc15c5a525 GIT binary patch literal 113557 zcmXtfWmH^E(`|xVaCf)h65O5O?(Xi+KyV%0-7Q#f5AF`ZgADHO3?F&kd;iSpHC?B> zYWJ?))n}bn`VI+&`3d3^+^0{UNIyj$wRRo_fBs}9kNFAq;~#r#S66FChtn=yS3;@O zk{AAGEH=UK#@n6`;Wzm;-Euic=k!&HIk~JWta#$0=n6kRg+nb?)1ABnx`{GKQ4@|v zr4{2MY~-GJ7-*S9wmlA6tTHvhZ0hl}^OQw_wb|JNxH)B}j^=re1_Hpx zJ-Q3%wbq>7jaw+Cx+*netxe@kk~!Gcl*cNKDU0awI@{@QcGRehF=2i9yEK-!IEd- zSLV)#>Hf{fe#0JLG4GS`2QH!5zt6{akkf|^hSQt+?=JoiEa0jo5+VNe9-_C!IFbv4 zxc8m=*Dj;dS{7+dda&i3fLG7nsse`8Ql~BK!bX~^ML-`x3k@wH)G7WN17fX zq}FJrt%syf6GG3!`i(lC6v%)h0P7}yu6z$4Hzbb!bp2+B;auLPe$W0=^*xrryZ;Ln z(r`a-kN4~tHiF22Lbr=A06X3KZ_h8OkYS3|1JzEi^@kDj62cU!2j2hW>; zq>VRfPq5?jKB#~CxBI=Li`4HMxU~N9p)xHYaqtOB?dB$u@LGT=Nq5)tW!eVs?jx9F zsRxkfJ^OIKs41n8Dx}DCvDx+fa8ebXCls2yany%jI=sCocKQO|l76~8_i^J(R^RLt zawggAnFM7W^Ey^aIX)})$JY0)33ktJbG5D&Y{XuiBMkVxgWeQDPW{ZC-OrzBhY6mX z-tIRz$>NyJ0~iHdOub&y`qy4BIyUw=9^a}D?`mruFD`0^x%#=*kv{itglB~t<{1{; zk0+5RE)941@sALYv?jcVa=q6X>i6{Q5Oy{NM)OVFo!c9}IX_-JFM_3n^!%3t1RstV z9lYL7JW)KS*?^-50KM6BpSR0}vxSnG82@$4-ti^Q>J)6e(j-lsi9@oIb1;heZ;GvJQO zdUA0ijRbssQuX%s3NB0Hdux>ugKBRhvcD6Vx|;`7B;g2kJ!EB}plHSsP{gg*KX`*{ zQAB6UzTm3lW?#�C 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

)uMH90pA3A9 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

TD^mME}euhjmyWg*}Oj7kS{Bj%Nb(nJ63x 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 literal 0 HcmV?d00001 diff --git a/tests/theater/test_controlpoint.py b/tests/theater/test_controlpoint.py index d3c490dc..ada9a471 100644 --- a/tests/theater/test_controlpoint.py +++ b/tests/theater/test_controlpoint.py @@ -17,6 +17,7 @@ from game.theater.controlpoint import ( OffMapSpawn, Fob, ParkingType, + Player, ) from game.utils import Heading @@ -30,8 +31,8 @@ def test_mission_types_friendly(mocker: Any) -> None: mocker.patch("game.theater.controlpoint.Airfield.is_friendly", return_value=True) airport = Airport(None, None) # type: ignore airport.name = "test" # required for Airfield.__init__ - airfield = Airfield(airport, theater=None, starts_blue=True) # type: ignore - mission_types = list(airfield.mission_types(for_player=True)) + airfield = Airfield(airport, theater=None, starts_blue=Player.BLUE) # type: ignore + mission_types = list(airfield.mission_types(for_player=Player.BLUE)) assert len(mission_types) == 3 assert FlightType.AEWC in mission_types assert FlightType.REFUELING in mission_types @@ -39,8 +40,8 @@ def test_mission_types_friendly(mocker: Any) -> None: # Carrier mocker.patch("game.theater.controlpoint.Carrier.is_friendly", return_value=True) - carrier = Carrier(name="test", at=None, theater=None, starts_blue=True) # type: ignore - mission_types = list(carrier.mission_types(for_player=True)) + carrier = Carrier(name="test", at=None, theater=None, starts_blue=Player.BLUE) # type: ignore + mission_types = list(carrier.mission_types(for_player=Player.BLUE)) assert len(mission_types) == 3 assert FlightType.AEWC in mission_types assert FlightType.REFUELING in mission_types @@ -48,8 +49,8 @@ def test_mission_types_friendly(mocker: Any) -> None: # LHA mocker.patch("game.theater.controlpoint.Lha.is_friendly", return_value=True) - lha = Lha(name="test", at=None, theater=None, starts_blue=True) # type: ignore - mission_types = list(lha.mission_types(for_player=True)) + lha = Lha(name="test", at=None, theater=None, starts_blue=Player.BLUE) # type: ignore + mission_types = list(lha.mission_types(for_player=Player.BLUE)) assert len(mission_types) == 3 assert FlightType.AEWC in mission_types assert FlightType.REFUELING in mission_types @@ -57,16 +58,16 @@ def test_mission_types_friendly(mocker: Any) -> None: # Fob mocker.patch("game.theater.controlpoint.Fob.is_friendly", return_value=True) - fob = Fob(name="test", at=None, theater=None, starts_blue=True) # type: ignore - mission_types = list(fob.mission_types(for_player=True)) + fob = Fob(name="test", at=None, theater=None, starts_blue=Player.BLUE) # type: ignore + mission_types = list(fob.mission_types(for_player=Player.BLUE)) assert len(mission_types) == 2 assert FlightType.AEWC in mission_types assert FlightType.BARCAP in mission_types # Off map spawn mocker.patch("game.theater.controlpoint.OffMapSpawn.is_friendly", return_value=True) - off_map_spawn = OffMapSpawn(name="test", position=None, theater=None, starts_blue=True) # type: ignore - mission_types = list(off_map_spawn.mission_types(for_player=True)) + off_map_spawn = OffMapSpawn(name="test", position=None, theater=None, starts_blue=Player.BLUE) # type: ignore + mission_types = list(off_map_spawn.mission_types(for_player=Player.BLUE)) assert len(mission_types) == 0 @@ -79,8 +80,8 @@ def test_mission_types_enemy(mocker: Any) -> None: mocker.patch("game.theater.controlpoint.Airfield.is_friendly", return_value=False) airport = Airport(None, None) # type: ignore airport.name = "test" # required for Airfield.__init__ - airfield = Airfield(airport, theater=None, starts_blue=True) # type: ignore - mission_types = list(airfield.mission_types(for_player=True)) + airfield = Airfield(airport, theater=None, starts_blue=Player.BLUE) # type: ignore + mission_types = list(airfield.mission_types(for_player=Player.BLUE)) assert len(mission_types) == 8 assert FlightType.OCA_AIRCRAFT in mission_types assert FlightType.OCA_RUNWAY in mission_types @@ -93,8 +94,8 @@ def test_mission_types_enemy(mocker: Any) -> None: # Carrier mocker.patch("game.theater.controlpoint.Carrier.is_friendly", return_value=False) - carrier = Carrier(name="test", at=None, theater=None, starts_blue=True) # type: ignore - mission_types = list(carrier.mission_types(for_player=True)) + carrier = Carrier(name="test", at=None, theater=None, starts_blue=Player.BLUE) # type: ignore + mission_types = list(carrier.mission_types(for_player=Player.BLUE)) assert len(mission_types) == 5 assert FlightType.ANTISHIP in mission_types assert FlightType.ESCORT in mission_types @@ -104,8 +105,8 @@ def test_mission_types_enemy(mocker: Any) -> None: # LHA mocker.patch("game.theater.controlpoint.Lha.is_friendly", return_value=False) - lha = Lha(name="test", at=None, theater=None, starts_blue=True) # type: ignore - mission_types = list(lha.mission_types(for_player=True)) + lha = Lha(name="test", at=None, theater=None, starts_blue=Player.BLUE) # type: ignore + mission_types = list(lha.mission_types(for_player=Player.BLUE)) assert len(mission_types) == 5 assert FlightType.ANTISHIP in mission_types assert FlightType.ESCORT in mission_types @@ -115,8 +116,8 @@ def test_mission_types_enemy(mocker: Any) -> None: # Fob mocker.patch("game.theater.controlpoint.Fob.is_friendly", return_value=False) - fob = Fob(name="test", at=None, theater=None, starts_blue=True) # type: ignore - mission_types = list(fob.mission_types(for_player=True)) + fob = Fob(name="test", at=None, theater=None, starts_blue=Player.BLUE) # type: ignore + mission_types = list(fob.mission_types(for_player=Player.BLUE)) assert len(mission_types) == 6 assert FlightType.AIR_ASSAULT in mission_types assert FlightType.ESCORT in mission_types @@ -129,8 +130,8 @@ def test_mission_types_enemy(mocker: Any) -> None: mocker.patch( "game.theater.controlpoint.OffMapSpawn.is_friendly", return_value=False ) - off_map_spawn = OffMapSpawn(name="test", position=None, theater=None, starts_blue=True) # type: ignore - mission_types = list(off_map_spawn.mission_types(for_player=True)) + off_map_spawn = OffMapSpawn(name="test", position=None, theater=None, starts_blue=Player.BLUE) # type: ignore + mission_types = list(off_map_spawn.mission_types(for_player=Player.BLUE)) assert len(mission_types) == 0 @@ -144,7 +145,7 @@ def test_control_point_parking(mocker: Any) -> None: airport = Airport(None, None) # type: ignore airport.name = "test" # required for Airfield.__init__ point = Point(0, 0, None) # type: ignore - control_point = Airfield(airport, theater=None, starts_blue=True) # type: ignore + control_point = Airfield(airport, theater=None, starts_blue=Player.BLUE) # type: ignore parking_type_ground_start = ParkingType( fixed_wing=False, fixed_wing_stol=True, rotary_wing=False ) diff --git a/tests/theater/test_theatergroundobject.py b/tests/theater/test_theatergroundobject.py index 5afc0ed6..b59d7b5d 100644 --- a/tests/theater/test_theatergroundobject.py +++ b/tests/theater/test_theatergroundobject.py @@ -4,7 +4,7 @@ import pytest from dcs.mapping import Point from game.ato.flighttype import FlightType -from game.theater.controlpoint import OffMapSpawn +from game.theater.controlpoint import OffMapSpawn, Player from game.theater.presetlocation import PresetLocation from game.theater.theatergroundobject import ( BuildingGroundObject, @@ -34,7 +34,7 @@ def test_mission_types_friendly(mocker: Any) -> None: name="dummy_control_point", position=Point(0, 0, None), # type: ignore theater=None, # type: ignore - starts_blue=True, + starts_blue=Player.BLUE, ) # Patch is_friendly as it's difficult to set up a proper ControlPoint @@ -56,7 +56,7 @@ def test_mission_types_friendly(mocker: Any) -> None: control_point=dummy_control_point, task=None, ) - mission_types = list(ground_object.mission_types(for_player=True)) + mission_types = list(ground_object.mission_types(for_player=Player.BLUE)) assert mission_types == [FlightType.BARCAP] for ground_object_type in [BuildingGroundObject, IadsBuildingGroundObject]: @@ -67,7 +67,7 @@ def test_mission_types_friendly(mocker: Any) -> None: control_point=dummy_control_point, task=None, ) - mission_types = list(ground_object.mission_types(for_player=True)) + mission_types = list(ground_object.mission_types(for_player=Player.BLUE)) assert mission_types == [FlightType.BARCAP] @@ -84,7 +84,7 @@ def test_mission_types_enemy(mocker: Any) -> None: name="dummy_control_point", position=Point(0, 0, None), # type: ignore theater=None, # type: ignore - starts_blue=True, + starts_blue=Player.BLUE, ) # Patch is_friendly as it's difficult to set up a proper ControlPoint @@ -99,7 +99,7 @@ def test_mission_types_enemy(mocker: Any) -> None: control_point=dummy_control_point, task=None, ) - mission_types = list(building.mission_types(for_player=False)) + mission_types = list(building.mission_types(for_player=Player.RED)) assert len(mission_types) == 6 assert FlightType.STRIKE in mission_types assert FlightType.REFUELING in mission_types @@ -115,7 +115,7 @@ def test_mission_types_enemy(mocker: Any) -> None: control_point=dummy_control_point, task=None, ) - mission_types = list(iads_building.mission_types(for_player=False)) + mission_types = list(iads_building.mission_types(for_player=Player.RED)) assert len(mission_types) == 7 assert FlightType.STRIKE in mission_types assert FlightType.REFUELING in mission_types @@ -136,7 +136,7 @@ def test_mission_types_enemy(mocker: Any) -> None: control_point=dummy_control_point, task=None, ) - mission_types = list(ground_object.mission_types(for_player=False)) + mission_types = list(ground_object.mission_types(for_player=Player.RED)) assert len(mission_types) == 7 assert FlightType.ANTISHIP in mission_types assert FlightType.STRIKE in mission_types @@ -152,7 +152,7 @@ def test_mission_types_enemy(mocker: Any) -> None: control_point=dummy_control_point, task=None, ) - mission_types = list(sam.mission_types(for_player=False)) + mission_types = list(sam.mission_types(for_player=Player.RED)) assert len(mission_types) == 8 assert FlightType.DEAD in mission_types assert FlightType.SEAD in mission_types @@ -168,7 +168,7 @@ def test_mission_types_enemy(mocker: Any) -> None: location=dummy_location, control_point=dummy_control_point, ) - mission_types = list(ewr.mission_types(for_player=False)) + mission_types = list(ewr.mission_types(for_player=Player.RED)) assert len(mission_types) == 7 assert FlightType.DEAD in mission_types assert FlightType.STRIKE in mission_types @@ -187,7 +187,7 @@ def test_mission_types_enemy(mocker: Any) -> None: location=dummy_location, control_point=dummy_control_point, ) - mission_types = list(ground_object.mission_types(for_player=False)) + mission_types = list(ground_object.mission_types(for_player=Player.RED)) assert len(mission_types) == 7 assert FlightType.BAI in mission_types assert FlightType.STRIKE in mission_types @@ -203,7 +203,7 @@ def test_mission_types_enemy(mocker: Any) -> None: control_point=dummy_control_point, task=None, ) - mission_types = list(vehicles.mission_types(for_player=False)) + mission_types = list(vehicles.mission_types(for_player=Player.RED)) assert len(mission_types) == 7 assert FlightType.BAI in mission_types assert FlightType.STRIKE in mission_types