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 00000000..66f1376b Binary files /dev/null and b/resources/campaigns/scenic_route_neutral.miz differ 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