Factor out Coalition from Game.

This commit is contained in:
Dan Albert 2021-07-12 16:10:12 -07:00
parent 4534758c21
commit 17c19d453b
34 changed files with 471 additions and 437 deletions

215
game/coalition.py Normal file
View File

@ -0,0 +1,215 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional
from dcs import Point
from faker import Faker
from game.commander import TheaterCommander
from game.income import Income
from game.navmesh import NavMesh
from game.profiling import logged_duration, MultiEventTracer
from game.threatzones import ThreatZones
from game.transfers import PendingTransfers
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
if TYPE_CHECKING:
from game import Game
from game.data.doctrine import Doctrine
from game.factions.faction import Faction
from game.procurement import AircraftProcurementRequest, ProcurementAi
from game.squadrons import AirWing
from game.theater.bullseye import Bullseye
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from gen import AirTaskingOrder
class Coalition:
def __init__(
self, game: Game, faction: Faction, budget: float, player: bool
) -> None:
self.game = game
self.player = player
self.faction = faction
self.budget = budget
self.ato = AirTaskingOrder()
self.transit_network = TransitNetwork()
self.procurement_requests: list[AircraftProcurementRequest] = []
self.bullseye = Bullseye(Point(0, 0))
self.faker = Faker(self.faction.locales)
self.air_wing = AirWing(game, self)
self.transfers = PendingTransfers(game, player)
# Late initialized because the two coalitions in the game are mutually
# dependent, so must be both constructed before this property can be set.
self._opponent: Optional[Coalition] = None
# Volatile properties that are not persisted to the save file since they can be
# recomputed on load. Keeping this data out of the save file makes save compat
# breaks less frequent. Each of these properties has a non-underscore-prefixed
# @property that should be used for non-Optional access.
#
# All of these are late-initialized (whether via on_load or called later), but
# will be non-None after the game has finished loading.
self._threat_zone: Optional[ThreatZones] = None
self._navmesh: Optional[NavMesh] = None
self.on_load()
@property
def doctrine(self) -> Doctrine:
return self.faction.doctrine
@property
def coalition_id(self) -> int:
if self.player:
return 2
return 1
@property
def country_name(self) -> str:
return self.faction.country
@property
def opponent(self) -> Coalition:
assert self._opponent is not None
return self._opponent
@property
def threat_zone(self) -> ThreatZones:
assert self._threat_zone is not None
return self._threat_zone
@property
def nav_mesh(self) -> NavMesh:
assert self._navmesh is not None
return self._navmesh
def __getstate__(self) -> dict[str, Any]:
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"]
del state["faker"]
return state
def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state)
# Regenerate any state that was not persisted.
self.on_load()
def on_load(self) -> None:
self.faker = Faker(self.faction.locales)
def set_opponent(self, opponent: Coalition) -> None:
if self._opponent is not None:
raise RuntimeError("Double-initialization of Coalition.opponent")
self._opponent = opponent
def adjust_budget(self, amount: float) -> None:
self.budget += amount
def compute_threat_zones(self) -> None:
self._threat_zone = ThreatZones.for_faction(self.game, self.player)
def compute_nav_meshes(self) -> None:
self._navmesh = NavMesh.from_threat_zones(
self.opponent.threat_zone, self.game.theater
)
def update_transit_network(self) -> None:
self.transit_network = TransitNetworkBuilder(
self.game.theater, self.player
).build()
def set_bullseye(self, bullseye: Bullseye) -> None:
self.bullseye = bullseye
def end_turn(self) -> None:
"""Processes coalition-specific turn finalization.
For more information on turn finalization in general, see the documentation for
`Game.finish_turn`.
"""
self.air_wing.replenish()
self.budget += Income(self.game, self.player).total
# Need to recompute before transfers and deliveries to account for captures.
# This happens in in initialize_turn as well, because cheating doesn't advance a
# turn but can capture bases so we need to recompute there as well.
self.update_transit_network()
# Must happen *before* unit deliveries are handled, or else new units will spawn
# one hop ahead. ControlPoint.process_turn handles unit deliveries. The
# coalition-specific turn-end happens before the theater-wide turn-end, so this
# is handled correctly.
self.transfers.perform_transfers()
def initialize_turn(self) -> None:
"""Processes coalition-specific turn initialization.
For more information on turn initialization in general, see the documentation
for `Game.initialize_turn`.
"""
# Needs to happen *before* planning transfers so we don't cancel them.
self.ato.clear()
self.air_wing.reset()
self.refund_outstanding_orders()
self.procurement_requests.clear()
with logged_duration("Transit network identification"):
self.update_transit_network()
with logged_duration("Procurement of airlift assets"):
self.transfers.order_airlift_assets()
with logged_duration("Transport planning"):
self.transfers.plan_transports()
self.plan_missions()
self.plan_procurement()
def refund_outstanding_orders(self) -> None:
# TODO: Split orders between air and ground units.
# This isn't quite right. If the player has ground purchases automated we should
# be refunding the ground units, and if they have air automated but not ground
# we should be refunding air units.
if self.player and not self.game.settings.automate_aircraft_reinforcements:
return
for cp in self.game.theater.control_points_for(self.player):
cp.pending_unit_deliveries.refund_all(self)
def plan_missions(self) -> None:
color = "Blue" if self.player else "Red"
with MultiEventTracer() as tracer:
mission_planner = CoalitionMissionPlanner(self.game, self.player)
with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"):
commander = TheaterCommander(self.game, self.player)
commander.plan_missions(mission_planner, tracer)
with tracer.trace(f"{color} mission fulfillment"):
mission_planner.fulfill_missions()
def plan_procurement(self) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much
# more of the budget that turn. Otherwise budget (after repairs) is split evenly
# between air and ground. For the default starting budget of 2000 this gives 600
# 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:
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
else:
manage_runways = False
manage_front_line = False
manage_aircraft = False
self.budget = ProcurementAi(
self.game,
self.player,
self.faction,
manage_runways,
manage_front_line,
manage_aircraft,
).spend_budget(self.budget)

View File

@ -14,9 +14,9 @@ from game.theater import (
Airfield,
)
from game.theater.theatergroundobject import (
NavalGroundObject,
BuildingGroundObject,
IadsGroundObject,
NavalGroundObject,
)
from game.transfers import CargoShip, Convoy
from game.utils import meters, nautical_miles
@ -163,13 +163,17 @@ class ObjectiveFinder:
def convoys(self) -> Iterator[Convoy]:
for front_line in self.front_lines():
yield from self.game.transfers.convoys.travelling_to(
yield from self.game.coalition_for(
self.is_player
).transfers.convoys.travelling_to(
front_line.control_point_hostile_to(self.is_player)
)
def cargo_ships(self) -> Iterator[CargoShip]:
for front_line in self.front_lines():
yield from self.game.transfers.cargo_ships.travelling_to(
yield from self.game.coalition_for(
self.is_player
).transfers.cargo_ships.travelling_to(
front_line.control_point_hostile_to(self.is_player)
)

View File

@ -6,7 +6,7 @@ from game.commander.missionproposals import EscortType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.data.doctrine import Doctrine
from game.theater import NavalGroundObject
from game.theater.theatergroundobject import NavalGroundObject
from gen.flights.flight import FlightType

View File

@ -18,7 +18,6 @@ from typing import (
Union,
)
from game import db
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater import Airfield, ControlPoint
@ -136,10 +135,8 @@ class Debriefing:
self.game = game
self.unit_map = unit_map
self.player_country = game.player_country
self.enemy_country = game.enemy_country
self.player_country_id = db.country_id_from_name(game.player_country)
self.enemy_country_id = db.country_id_from_name(game.enemy_country)
self.player_country = game.blue.country_name
self.enemy_country = game.red.country_name
self.air_losses = self.dead_aircraft()
self.ground_losses = self.dead_ground_units()

View File

@ -53,7 +53,7 @@ class Event:
@property
def is_player_attacking(self) -> bool:
return self.attacker_name == self.game.player_faction.name
return self.attacker_name == self.game.blue.faction.name
@property
def tasks(self) -> List[Type[Task]]:
@ -114,10 +114,10 @@ class Event:
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
self._transfer_aircraft(
self.game.blue_ato, debriefing.air_losses, for_player=True
self.game.blue.ato, debriefing.air_losses, for_player=True
)
self._transfer_aircraft(
self.game.red_ato, debriefing.air_losses, for_player=False
self.game.red.ato, debriefing.air_losses, for_player=False
)
def commit_air_losses(self, debriefing: Debriefing) -> None:
@ -154,8 +154,8 @@ class Event:
pilot.record.missions_flown += 1
def commit_pilot_experience(self) -> None:
self._commit_pilot_experience(self.game.blue_ato)
self._commit_pilot_experience(self.game.red_ato)
self._commit_pilot_experience(self.game.blue.ato)
self._commit_pilot_experience(self.game.red.ato)
@staticmethod
def commit_front_line_losses(debriefing: Debriefing) -> None:

View File

@ -1,13 +1,11 @@
import itertools
import logging
import math
import random
import sys
from collections import Iterator
from datetime import date, datetime, timedelta
from enum import Enum
from typing import Any, List, Type, Union, cast
from dcs.action import Coalition
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
@ -19,28 +17,25 @@ from game.plugins import LuaPluginManager
from gen import naming
from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner import GroundPlanner
from . import persistency
from .commander import TheaterCommander
from .coalition import Coalition
from .debriefing import Debriefing
from .event.event import Event
from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction
from .income import Income
from .infos.information import Information
from .navmesh import NavMesh
from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration, MultiEventTracer
from .settings import Settings, AutoAtoBehavior
from .procurement import AircraftProcurementRequest
from .profiling import logged_duration
from .settings import Settings
from .squadrons import AirWing
from .theater import ConflictTheater, ControlPoint
from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .threatzones import ThreatZones
from .transfers import PendingTransfers
from .unitmap import UnitMap
from .weather import Conditions, TimeOfDay
@ -98,10 +93,6 @@ class Game:
self.settings = settings
self.events: List[Event] = []
self.theater = theater
self.player_faction = player_faction
self.player_country = player_faction.country
self.enemy_faction = enemy_faction
self.enemy_country = enemy_faction.country
# pass_turn() will be called when initialization is complete which will
# increment this to turn 0 before it reaches the player.
self.turn = -1
@ -124,108 +115,70 @@ class Game:
self.conditions = self.generate_conditions()
self.blue_transit_network = TransitNetwork()
self.red_transit_network = TransitNetwork()
self.blue_procurement_requests: List[AircraftProcurementRequest] = []
self.red_procurement_requests: List[AircraftProcurementRequest] = []
self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder()
self.blue_bullseye = Bullseye(Point(0, 0))
self.red_bullseye = Bullseye(Point(0, 0))
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.set_opponent(self.red)
self.red.set_opponent(self.blue)
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self.transfers = PendingTransfers(self)
self.sanitize_sides()
self.blue_faker = Faker(self.player_faction.locales)
self.red_faker = Faker(self.enemy_faction.locales)
self.blue_air_wing = AirWing(self, player=True)
self.red_air_wing = AirWing(self, player=False)
self.on_load(game_still_initializing=True)
def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
# Avoid persisting any volatile types that can be deterministically
# recomputed on load for the sake of save compatibility.
del state["blue_threat_zone"]
del state["red_threat_zone"]
del state["blue_navmesh"]
del state["red_navmesh"]
del state["blue_faker"]
del state["red_faker"]
return state
def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state)
# Regenerate any state that was not persisted.
self.on_load()
@property
def coalitions(self) -> Iterator[Coalition]:
yield self.blue
yield self.red
def ato_for(self, player: bool) -> AirTaskingOrder:
if player:
return self.blue_ato
return self.red_ato
return self.coalition_for(player).ato
def procurement_requests_for(
self, player: bool
) -> List[AircraftProcurementRequest]:
if player:
return self.blue_procurement_requests
return self.red_procurement_requests
) -> list[AircraftProcurementRequest]:
return self.coalition_for(player).procurement_requests
def transit_network_for(self, player: bool) -> TransitNetwork:
if player:
return self.blue_transit_network
return self.red_transit_network
return self.coalition_for(player).transit_network
def generate_conditions(self) -> Conditions:
return Conditions.generate(
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
)
def sanitize_sides(self) -> None:
@staticmethod
def sanitize_sides(player_faction: Faction, enemy_faction: Faction) -> None:
"""
Make sure the opposing factions are using different countries
:return:
"""
if self.player_country == self.enemy_country:
if self.player_country == "USA":
self.enemy_country = "USAF Aggressors"
elif self.player_country == "Russia":
self.enemy_country = "USSR"
if player_faction.country == enemy_faction.country:
if player_faction.country == "USA":
enemy_faction.country = "USAF Aggressors"
elif player_faction.country == "Russia":
enemy_faction.country = "USSR"
else:
self.enemy_country = "Russia"
enemy_faction.country = "Russia"
def faction_for(self, player: bool) -> Faction:
if player:
return self.player_faction
return self.enemy_faction
return self.coalition_for(player).faction
def faker_for(self, player: bool) -> Faker:
if player:
return self.blue_faker
return self.red_faker
return self.coalition_for(player).faker
def air_wing_for(self, player: bool) -> AirWing:
if player:
return self.blue_air_wing
return self.red_air_wing
return self.coalition_for(player).air_wing
def country_for(self, player: bool) -> str:
if player:
return self.player_country
return self.enemy_country
return self.coalition_for(player).country_name
def bullseye_for(self, player: bool) -> Bullseye:
if player:
return self.blue_bullseye
return self.red_bullseye
return self.coalition_for(player).bullseye
def _generate_player_event(
self, event_class: Type[Event], player_cp: ControlPoint, enemy_cp: ControlPoint
@ -236,8 +189,8 @@ class Game:
player_cp,
enemy_cp,
enemy_cp.position,
self.player_faction.name,
self.enemy_faction.name,
self.blue.faction.name,
self.red.faction.name,
)
)
@ -249,20 +202,13 @@ class Game:
front_line.red_cp,
)
def adjust_budget(self, amount: float, player: bool) -> None:
def coalition_for(self, player: bool) -> Coalition:
if player:
self.budget += amount
else:
self.enemy_budget += amount
return self.blue
return self.red
def process_player_income(self) -> None:
self.budget += Income(self, player=True).total
def process_enemy_income(self) -> None:
# TODO: Clean up save compat.
if not hasattr(self, "enemy_budget"):
self.enemy_budget = 0
self.enemy_budget += Income(self, player=False).total
def adjust_budget(self, amount: float, player: bool) -> None:
self.coalition_for(player).adjust_budget(amount)
@staticmethod
def initiate_event(event: Event) -> UnitMap:
@ -293,12 +239,6 @@ class Game:
self.compute_conflicts_position()
if not game_still_initializing:
self.compute_threat_zones()
self.blue_faker = Faker(self.faction_for(player=True).locales)
self.red_faker = Faker(self.faction_for(player=False).locales)
def reset_ato(self) -> None:
self.blue_ato.clear()
self.red_ato.clear()
def finish_turn(self, skipped: bool = False) -> None:
"""Finalizes the current turn and advances to the next turn.
@ -333,23 +273,16 @@ class Game:
)
self.turn += 1
# Need to recompute before transfers and deliveries to account for captures.
# This happens in in initialize_turn as well, because cheating doesn't advance a
# turn but can capture bases so we need to recompute there as well.
self.compute_transit_networks()
# The coalition-specific turn finalization *must* happen before unit deliveries,
# since the coalition-specific finalization handles transit network updates and
# transfer processing. If in the other order, units may be delivered to captured
# bases, and freshly delivered units will spawn one leg through their journey.
self.blue.end_turn()
self.red.end_turn()
# Must happen *before* unit deliveries are handled, or else new units will spawn
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
self.transfers.perform_transfers()
# Needs to happen *before* planning transfers so we don't cancel them.
self.reset_ato()
for control_point in self.theater.controlpoints:
control_point.process_turn(self)
self.blue_air_wing.replenish()
self.red_air_wing.replenish()
if not skipped:
for cp in self.theater.player_points():
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
@ -360,9 +293,6 @@ class Game:
self.conditions = self.generate_conditions()
self.process_enemy_income()
self.process_player_income()
def begin_turn_0(self) -> None:
"""Initialization for the first turn of the game."""
self.turn = 0
@ -402,8 +332,8 @@ class Game:
def set_bullseye(self) -> None:
player_cp, enemy_cp = self.theater.closest_opposing_control_points()
self.blue_bullseye = Bullseye(enemy_cp.position)
self.red_bullseye = Bullseye(player_cp.position)
self.blue.bullseye = Bullseye(enemy_cp.position)
self.red.bullseye = Bullseye(player_cp.position)
def initialize_turn(self, for_red: bool = True, for_blue: bool = True) -> None:
"""Performs turn initialization for the specified players.
@ -451,13 +381,20 @@ class Game:
if turn_state in (TurnState.LOSS, TurnState.WIN):
return self.process_win_loss(turn_state)
# Plan flights & combat for next turn
with logged_duration("Computing conflict positions"):
self.compute_conflicts_position()
with logged_duration("Threat zone computation"):
self.compute_threat_zones()
# Plan Coalition specific turn
if for_red:
self.initialize_turn_for(player=False)
if for_blue:
self.initialize_turn_for(player=True)
if for_red:
self.initialize_turn_for(player=False)
# Plan GroundWar
self.ground_planners = {}
for cp in self.theater.controlpoints:
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
@ -465,83 +402,10 @@ class Game:
self.ground_planners[cp.id] = gplanner
def initialize_turn_for(self, player: bool) -> None:
"""Processes coalition-specific turn initialization.
For more information on turn initialization in general, see the documentation
for `Game.initialize_turn`.
Args:
player: True if the player coalition is being initialized. False for opfor
initialization.
"""
self.ato_for(player).clear()
self.air_wing_for(player).reset()
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
self.aircraft_inventory.reset(player)
for cp in self.theater.control_points_for(player):
self.aircraft_inventory.set_from_control_point(cp)
# Refund all pending deliveries for opfor and if player
# has automate_aircraft_reinforcements
if (not player and not cp.captured) or (
player
and cp.captured
and self.settings.automate_aircraft_reinforcements
):
cp.pending_unit_deliveries.refund_all(self)
# Plan flights & combat for next turn
with logged_duration("Computing conflict positions"):
self.compute_conflicts_position()
with logged_duration("Threat zone computation"):
self.compute_threat_zones()
with logged_duration("Transit network identification"):
self.compute_transit_networks()
self.ground_planners = {}
self.procurement_requests_for(player).clear()
with logged_duration("Procurement of airlift assets"):
self.transfers.order_airlift_assets()
with logged_duration("Transport planning"):
self.transfers.plan_transports()
color = "Blue" if player else "Red"
with MultiEventTracer() as tracer:
mission_planner = CoalitionMissionPlanner(self, player)
with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"):
commander = TheaterCommander(self, player)
commander.plan_missions(mission_planner, tracer)
with tracer.trace(f"{color} mission fulfillment"):
mission_planner.fulfill_missions()
self.plan_procurement_for(player)
def plan_procurement_for(self, for_player: bool) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
# gets much more of the budget that turn. Otherwise budget (after
# repairs) is split evenly between air and ground. For the default
# starting budget of 2000 this gives 600 to ground forces and 1400 to
# aircraft. After that the budget will be spend proportionally based on how much is already invested
if for_player:
self.budget = ProcurementAi(
self,
for_player=True,
faction=self.player_faction,
manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements,
).spend_budget(self.budget)
else:
self.enemy_budget = ProcurementAi(
self,
for_player=False,
faction=self.enemy_faction,
manage_runways=True,
manage_front_line=True,
manage_aircraft=True,
).spend_budget(self.enemy_budget)
self.coalition_for(player).initialize_turn()
def message(self, text: str) -> None:
self.informations.append(Information(text, turn=self.turn))
@ -568,32 +432,20 @@ class Game:
self.current_group_id += 1
return self.current_group_id
def compute_transit_networks(self) -> None:
self.blue_transit_network = self.compute_transit_network_for(player=True)
self.red_transit_network = self.compute_transit_network_for(player=False)
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
return TransitNetworkBuilder(self.theater, player).build()
def compute_threat_zones(self) -> None:
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
self.blue_navmesh = NavMesh.from_threat_zones(
self.red_threat_zone, self.theater
)
self.red_navmesh = NavMesh.from_threat_zones(
self.blue_threat_zone, self.theater
)
self.blue.compute_threat_zones()
self.red.compute_threat_zones()
self.blue.compute_nav_meshes()
self.red.compute_nav_meshes()
def threat_zone_for(self, player: bool) -> ThreatZones:
if player:
return self.blue_threat_zone
return self.red_threat_zone
return self.coalition_for(player).threat_zone
def navmesh_for(self, player: bool) -> NavMesh:
if player:
return self.blue_navmesh
return self.red_navmesh
return self.coalition_for(player).nav_mesh
def compute_conflicts_position(self) -> None:
"""
@ -636,7 +488,7 @@ class Game:
if cpoint is not None:
zones.append(cpoint)
packages = itertools.chain(self.blue_ato.packages, self.red_ato.packages)
packages = itertools.chain(self.blue.ato.packages, self.red.ato.packages)
for package in packages:
if package.primary_task is FlightType.BARCAP:
# BARCAPs will be planned at most locations on smaller theaters,
@ -682,25 +534,6 @@ class Game:
"""
return self.__culling_zones
# 1 = red, 2 = blue
def get_player_coalition_id(self) -> int:
return 2
def get_enemy_coalition_id(self) -> int:
return 1
def get_player_coalition(self) -> Coalition:
return Coalition.Blue
def get_enemy_coalition(self) -> Coalition:
return Coalition.Red
def get_player_color(self) -> str:
return "blue"
def get_enemy_color(self) -> str:
return "red"
def process_win_loss(self, turn_state: TurnState) -> None:
if turn_state is TurnState.WIN:
self.message(

View File

@ -2,9 +2,7 @@
from __future__ import annotations
from collections import defaultdict
from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
from dcs.unittype import FlyingType
from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING
from game.dcs.aircrafttype import AircraftType
from gen.flights.flight import Flight
@ -86,10 +84,11 @@ class GlobalAircraftInventory:
cp: ControlPointAircraftInventory(cp) for cp in control_points
}
def reset(self) -> None:
"""Clears all control points and their inventories."""
def reset(self, for_player: bool) -> None:
"""Clears the inventory of every control point owned by the given coalition."""
for inventory in self.inventories.values():
inventory.clear()
if inventory.control_point.captured == for_player:
inventory.clear()
def set_from_control_point(self, control_point: ControlPoint) -> None:
"""Set the control point's aircraft inventory.

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import os
from pathlib import Path
from typing import Iterable, List, Set, TYPE_CHECKING, cast
from typing import List, Set, TYPE_CHECKING, cast
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
@ -81,10 +81,10 @@ class Operation:
return Conflict(
cls.game.theater,
FrontLine(player_cp, enemy_cp),
cls.game.player_faction.name,
cls.game.enemy_faction.name,
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country),
cls.game.blue.faction.name,
cls.game.red.faction.name,
cls.current_mission.country(cls.game.blue.country_name),
cls.current_mission.country(cls.game.red.country_name),
mid_point,
)
@ -95,14 +95,14 @@ class Operation:
@classmethod
def _setup_mission_coalitions(cls) -> None:
cls.current_mission.coalition["blue"] = Coalition(
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
"blue", bullseye=cls.game.blue.bullseye.to_pydcs()
)
cls.current_mission.coalition["red"] = Coalition(
"red", bullseye=cls.game.red_bullseye.to_pydcs()
"red", bullseye=cls.game.red.bullseye.to_pydcs()
)
p_country = cls.game.player_country
e_country = cls.game.enemy_country
p_country = cls.game.blue.country_name
e_country = cls.game.red.country_name
cls.current_mission.coalition["blue"].add_country(
country_dict[db.country_id_from_name(p_country)]()
)
@ -268,7 +268,7 @@ class Operation:
and cls.game.settings.perf_destroyed_units
):
cls.current_mission.static_group(
country=cls.current_mission.country(cls.game.player_country),
country=cls.current_mission.country(cls.game.blue.country_name),
name="",
_type=utype,
hidden=True,
@ -358,18 +358,18 @@ class Operation:
cls.airgen.clear_parking_slots()
cls.airgen.generate_flights(
cls.current_mission.country(cls.game.player_country),
cls.game.blue_ato,
cls.current_mission.country(cls.game.blue.country_name),
cls.game.blue.ato,
cls.groundobjectgen.runways,
)
cls.airgen.generate_flights(
cls.current_mission.country(cls.game.enemy_country),
cls.game.red_ato,
cls.current_mission.country(cls.game.red.country_name),
cls.game.red.ato,
cls.groundobjectgen.runways,
)
cls.airgen.spawn_unused_aircraft(
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country),
cls.current_mission.country(cls.game.blue.country_name),
cls.current_mission.country(cls.game.red.country_name),
)
@classmethod
@ -380,10 +380,10 @@ class Operation:
player_cp = front_line.blue_cp
enemy_cp = front_line.red_cp
conflict = Conflict.frontline_cas_conflict(
cls.game.player_faction.name,
cls.game.enemy_faction.name,
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country),
cls.game.blue.faction.name,
cls.game.red.faction.name,
cls.current_mission.country(cls.game.blue.country_name),
cls.current_mission.country(cls.game.red.country_name),
front_line,
cls.game.theater,
)

View File

@ -72,7 +72,9 @@ class ProcurementAi:
return 1
for cp in self.owned_points:
cp_ground_units = cp.allocated_ground_units(self.game.transfers)
cp_ground_units = cp.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers
)
armor_investment += cp_ground_units.total_value
cp_aircraft = cp.allocated_aircraft(self.game)
aircraft_investment += cp_aircraft.total_value
@ -316,7 +318,9 @@ class ProcurementAi:
continue
purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR
allocated = cp.allocated_ground_units(self.game.transfers)
allocated = cp.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers
)
if allocated.total >= purchase_target:
# Control point is already sufficiently defended.
continue
@ -343,7 +347,9 @@ class ProcurementAi:
if not cp.can_recruit_ground_units(self.game):
continue
allocated = cp.allocated_ground_units(self.game.transfers)
allocated = cp.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers
)
if allocated.total >= self.game.settings.reserves_procurement_target:
continue
@ -356,7 +362,9 @@ class ProcurementAi:
def cost_ratio_of_ground_unit(
self, control_point: ControlPoint, unit_class: GroundUnitClass
) -> float:
allocations = control_point.allocated_ground_units(self.game.transfers)
allocations = control_point.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers
)
class_cost = 0
total_cost = 0
for unit_type, count in allocations.all.items():

View File

@ -20,10 +20,11 @@ import yaml
from faker import Faker
from game.dcs.aircrafttype import AircraftType
from game.settings import AutoAtoBehavior
from game.settings import AutoAtoBehavior, Settings
if TYPE_CHECKING:
from game import Game
from game.coalition import Coalition
from gen.flights.flight import FlightType
@ -96,16 +97,13 @@ class Squadron:
init=False, hash=False, compare=False
)
# We need a reference to the Game so that we can access the Faker without needing to
# persist it to the save game, or having to reconstruct it (it's not cheap) each
# time we create or load a squadron.
game: Game = field(hash=False, compare=False)
player: bool
coalition: Coalition = field(hash=False, compare=False)
settings: Settings = field(hash=False, compare=False)
def __post_init__(self) -> None:
if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
raise ValueError("Squadrons can only be created with active pilots.")
self._recruit_pilots(self.game.settings.squadron_pilot_limit)
self._recruit_pilots(self.settings.squadron_pilot_limit)
self.auto_assignable_mission_types = set(self.mission_types)
def __str__(self) -> str:
@ -113,9 +111,13 @@ class Squadron:
return self.name
return f'{self.name} "{self.nickname}"'
@property
def player(self) -> bool:
return self.coalition.player
@property
def pilot_limits_enabled(self) -> bool:
return self.game.settings.enable_squadron_pilot_limits
return self.settings.enable_squadron_pilot_limits
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
if self.pilot_limits_enabled:
@ -131,7 +133,7 @@ class Squadron:
if not self.player:
return self.available_pilots.pop()
preference = self.game.settings.auto_ato_behavior
preference = self.settings.auto_ato_behavior
# No preference, so the first pilot is fine.
if preference is AutoAtoBehavior.Default:
@ -184,7 +186,7 @@ class Squadron:
return
replenish_count = min(
self.game.settings.squadron_replenishment_rate,
self.settings.squadron_replenishment_rate,
self._number_of_unfilled_pilot_slots,
)
if replenish_count > 0:
@ -206,7 +208,7 @@ class Squadron:
@property
def faker(self) -> Faker:
return self.game.faker_for(self.player)
return self.coalition.faker
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.current_roster if p.status == status]
@ -228,7 +230,7 @@ class Squadron:
@property
def _number_of_unfilled_pilot_slots(self) -> int:
return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
return self.settings.squadron_pilot_limit - len(self.active_pilots)
@property
def number_of_available_pilots(self) -> int:
@ -252,7 +254,7 @@ class Squadron:
return self.current_roster[index]
@classmethod
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
def from_yaml(cls, path: Path, game: Game, coalition: Coalition) -> Squadron:
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
from gen.flights.flight import FlightType
@ -287,8 +289,8 @@ class Squadron:
livery=data.get("livery"),
mission_types=tuple(mission_types),
pilot_pool=pilots,
game=game,
player=player,
coalition=coalition,
settings=game.settings,
)
def __setstate__(self, state: dict[str, Any]) -> None:
@ -299,9 +301,9 @@ class Squadron:
class SquadronLoader:
def __init__(self, game: Game, player: bool) -> None:
def __init__(self, game: Game, coalition: Coalition) -> None:
self.game = game
self.player = player
self.coalition = coalition
@staticmethod
def squadron_directories() -> Iterator[Path]:
@ -312,8 +314,8 @@ class SquadronLoader:
def load(self) -> dict[AircraftType, list[Squadron]]:
squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
country = self.game.country_for(self.player)
faction = self.game.faction_for(self.player)
country = self.coalition.country_name
faction = self.coalition.faction
any_country = country.startswith("Combined Joint Task Forces ")
for directory in self.squadron_directories():
for path, squadron in self.load_squadrons_from(directory):
@ -347,7 +349,7 @@ class SquadronLoader:
for squadron_path in directory.glob("*/*.yaml"):
try:
yield squadron_path, Squadron.from_yaml(
squadron_path, self.game, self.player
squadron_path, self.game, self.coalition
)
except Exception as ex:
raise RuntimeError(
@ -356,29 +358,28 @@ class SquadronLoader:
class AirWing:
def __init__(self, game: Game, player: bool) -> None:
def __init__(self, game: Game, coalition: Coalition) -> None:
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
self.game = game
self.player = player
self.squadrons = SquadronLoader(game, player).load()
self.squadrons = SquadronLoader(game, coalition).load()
count = itertools.count(1)
for aircraft in game.faction_for(player).aircrafts:
for aircraft in coalition.faction.aircrafts:
if aircraft in self.squadrons:
continue
self.squadrons[aircraft] = [
Squadron(
name=f"Squadron {next(count):03}",
nickname=self.random_nickname(),
country=game.country_for(player),
country=coalition.country_name,
role="Flying Squadron",
aircraft=aircraft,
livery=None,
mission_types=tuple(tasks_for_aircraft(aircraft)),
pilot_pool=[],
game=game,
player=player,
coalition=coalition,
settings=game.settings,
)
]

View File

@ -40,11 +40,7 @@ from gen.ground_forces.combat_stance import CombatStance
from gen.runways import RunwayAssigner, RunwayData
from .base import Base
from .missiontarget import MissionTarget
from .theatergroundobject import (
GenericCarrierGroundObject,
TheaterGroundObject,
NavalGroundObject,
)
from .theatergroundobject import GenericCarrierGroundObject, TheaterGroundObject
from ..dcs.aircrafttype import AircraftType
from ..dcs.groundunittype import GroundUnitType
from ..utils import nautical_miles
@ -606,7 +602,7 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Should be Airbase specific.
def capture(self, game: Game, for_player: bool) -> None:
self.pending_unit_deliveries.refund_all(game)
self.pending_unit_deliveries.refund_all(game.coalition_for(for_player))
self.retreat_ground_units(game)
self.retreat_air_units(game)
self.depopulate_uncapturable_tgos()
@ -623,11 +619,7 @@ class ControlPoint(MissionTarget, ABC):
...
def aircraft_transferring(self, game: Game) -> dict[AircraftType, int]:
if self.captured:
ato = game.blue_ato
else:
ato = game.red_ato
ato = game.coalition_for(self.captured).ato
transferring: defaultdict[AircraftType, int] = defaultdict(int)
for package in ato.packages:
for flight in package.flights:

View File

@ -11,7 +11,7 @@ from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
from game import Game, db
from game import Game
from game.factions.faction import Faction
from game.scenery_group import SceneryGroup
from game.theater import Carrier, Lha, PointWithHeading
@ -171,14 +171,11 @@ class ControlPointGroundObjectGenerator:
@property
def faction_name(self) -> str:
if self.control_point.captured:
return self.game.player_faction.name
else:
return self.game.enemy_faction.name
return self.faction.name
@property
def faction(self) -> Faction:
return db.FACTIONS[self.faction_name]
return self.game.coalition_for(self.control_point.captured).faction
def generate(self) -> bool:
self.control_point.connected_objectives = []

View File

@ -316,7 +316,9 @@ class AirliftPlanner:
capacity = flight_size * capacity_each
if capacity < self.transfer.size:
transfer = self.game.transfers.split_transfer(self.transfer, capacity)
transfer = self.game.coalition_for(
self.for_player
).transfers.split_transfer(self.transfer, capacity)
else:
transfer = self.transfer
@ -534,8 +536,9 @@ class CargoShipMap(TransportMap[CargoShip]):
class PendingTransfers:
def __init__(self, game: Game) -> None:
def __init__(self, game: Game, player: bool) -> None:
self.game = game
self.player = player
self.convoys = ConvoyMap()
self.cargo_ships = CargoShipMap()
self.pending_transfers: List[TransferOrder] = []
@ -609,7 +612,7 @@ class PendingTransfers:
flight = transport.flight
flight.package.remove_flight(flight)
if not flight.package.flights:
self.game.ato_for(transport.player_owned).remove_package(flight.package)
self.game.ato_for(self.player).remove_package(flight.package)
self.game.aircraft_inventory.return_from_flight(flight)
flight.clear_roster()
@ -647,7 +650,7 @@ class PendingTransfers:
self.arrange_transport(transfer)
def order_airlift_assets(self) -> None:
for control_point in self.game.theater.controlpoints:
for control_point in self.game.theater.control_points_for(self.player):
if self.game.air_wing_for(control_point.captured).can_auto_plan(
FlightType.TRANSPORT
):
@ -682,7 +685,7 @@ class PendingTransfers:
# aesthetic.
gap += 1
self.game.procurement_requests_for(player=control_point.captured).append(
self.game.procurement_requests_for(self.player).append(
AircraftProcurementRequest(
control_point, nautical_miles(200), FlightType.TRANSPORT, gap
)

View File

@ -6,6 +6,7 @@ from dataclasses import dataclass
from typing import Optional, TYPE_CHECKING, Any
from game.theater import ControlPoint
from .coalition import Coalition
from .dcs.groundunittype import GroundUnitType
from .dcs.unittype import UnitType
from .theater.transitnetwork import (
@ -41,24 +42,22 @@ class PendingUnitDeliveries:
for k, v in units.items():
self.units[k] -= v
def refund_all(self, game: Game) -> None:
self.refund(game, self.units)
def refund_all(self, coalition: Coalition) -> None:
self.refund(coalition, self.units)
self.units = defaultdict(int)
def refund_ground_units(self, game: Game) -> None:
def refund_ground_units(self, coalition: Coalition) -> None:
ground_units: dict[UnitType[Any], int] = {
u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType)
}
self.refund(game, ground_units)
self.refund(coalition, ground_units)
for gu in ground_units.keys():
del self.units[gu]
def refund(self, game: Game, units: dict[UnitType[Any], int]) -> None:
def refund(self, coalition: Coalition, units: dict[UnitType[Any], int]) -> None:
for unit_type, count in units.items():
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
game.adjust_budget(
unit_type.price * count, player=self.destination.captured
)
coalition.adjust_budget(unit_type.price * count)
def pending_orders(self, unit_type: UnitType[Any]) -> int:
pending_units = self.units.get(unit_type)
@ -71,19 +70,20 @@ class PendingUnitDeliveries:
return self.pending_orders(unit_type) + current_units
def process(self, game: Game) -> None:
coalition = game.coalition_for(self.destination.captured)
ground_unit_source = self.find_ground_unit_source(game)
if ground_unit_source is None:
game.message(
f"{self.destination.name} lost its source for ground unit "
"reinforcements. Refunding purchase price."
)
self.refund_ground_units(game)
self.refund_ground_units(coalition)
bought_units: dict[UnitType[Any], int] = {}
units_needing_transfer: dict[GroundUnitType, int] = {}
sold_units: dict[UnitType[Any], int] = {}
for unit_type, count in self.units.items():
coalition = "Ally" if self.destination.captured else "Enemy"
allegiance = "Ally" if self.destination.captured else "Enemy"
d: dict[Any, int]
if (
isinstance(unit_type, GroundUnitType)
@ -98,11 +98,11 @@ class PendingUnitDeliveries:
if count >= 0:
d[unit_type] = count
game.message(
f"{coalition} reinforcements: {unit_type} x {count} at {source}"
f"{allegiance} reinforcements: {unit_type} x {count} at {source}"
)
else:
sold_units[unit_type] = -count
game.message(f"{coalition} sold: {unit_type} x {-count} at {source}")
game.message(f"{allegiance} sold: {unit_type} x {-count} at {source}")
self.units = defaultdict(int)
self.destination.base.commission_units(bought_units)
@ -111,16 +111,19 @@ class PendingUnitDeliveries:
if units_needing_transfer:
if ground_unit_source is None:
raise RuntimeError(
f"ground unit source could not be found for {self.destination} but still tried to "
f"transfer units to there"
f"Ground unit source could not be found for {self.destination} but "
"still tried to transfer units to there"
)
ground_unit_source.base.commission_units(units_needing_transfer)
self.create_transfer(game, ground_unit_source, units_needing_transfer)
self.create_transfer(coalition, ground_unit_source, units_needing_transfer)
def create_transfer(
self, game: Game, source: ControlPoint, units: dict[GroundUnitType, int]
self,
coalition: Coalition,
source: ControlPoint,
units: dict[GroundUnitType, int],
) -> None:
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
coalition.transfers.new_transfer(TransferOrder(source, self.destination, units))
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
# This is running *after* the turn counter has been incremented, so this is the

View File

@ -22,7 +22,6 @@ from dcs.planes import (
C_101EB,
F_14B,
JF_17,
PlaneType,
Su_33,
Tu_22M3,
)
@ -262,8 +261,8 @@ class AircraftConflictGenerator:
@cached_property
def use_client(self) -> bool:
"""True if Client should be used instead of Player."""
blue_clients = self.client_slots_in_ato(self.game.blue_ato)
red_clients = self.client_slots_in_ato(self.game.red_ato)
blue_clients = self.client_slots_in_ato(self.game.blue.ato)
red_clients = self.client_slots_in_ato(self.game.red.ato)
return blue_clients + red_clients > 1
@staticmethod
@ -601,12 +600,11 @@ class AircraftConflictGenerator:
if not isinstance(control_point, Airfield):
continue
faction = self.game.coalition_for(control_point.captured).faction
if control_point.captured:
country = player_country
faction = self.game.player_faction
else:
country = enemy_country
faction = self.game.enemy_faction
for aircraft, available in inventory.all_aircraft:
try:
@ -699,11 +697,7 @@ class AircraftConflictGenerator:
if flight.from_cp.cptype != ControlPointType.AIRBASE:
return
if flight.from_cp.captured:
coalition = self.game.get_player_coalition_id()
else:
coalition = self.game.get_enemy_coalition_id()
coalition = self.game.coalition_for(flight.departure.captured).coalition_id
trigger.add_condition(CoalitionHasAirdrome(coalition, flight.from_cp.id))
def generate_planned_flight(

View File

@ -105,6 +105,8 @@ class AirSupportConflictGenerator:
else self.conflict.red_cp
)
country = self.mission.country(self.game.blue.country_name)
if not self.game.settings.disable_legacy_tanker:
fallback_tanker_number = 0
@ -130,10 +132,8 @@ class AirSupportConflictGenerator:
tanker_heading, TANKER_DISTANCE
)
tanker_group = self.mission.refuel_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_tanker_name(
self.mission.country(self.game.player_country), tanker_unit_type
),
country=country,
name=namegen.next_tanker_name(country, tanker_unit_type),
airport=None,
plane_type=unit_type,
position=tanker_position,
@ -211,10 +211,8 @@ class AirSupportConflictGenerator:
return
awacs_flight = self.mission.awacs_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_awacs_name(
self.mission.country(self.game.player_country)
),
country=country,
name=namegen.next_awacs_name(country),
plane_type=unit_type,
altitude=AWACS_ALT,
airport=None,

View File

@ -144,16 +144,16 @@ class GroundConflictGenerator:
)
# Add JTAC
if self.game.player_faction.has_jtac:
if self.game.blue.faction.has_jtac:
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
code = 1688 - len(self.jtacs)
utype = self.game.player_faction.jtac_unit
utype = self.game.blue.faction.jtac_unit
if utype is None:
utype = AircraftType.named("MQ-9 Reaper")
jtac = self.mission.flight_group(
country=self.mission.country(self.game.player_country),
country=self.mission.country(self.game.blue.country_name),
name=n,
aircraft_type=utype.dcs_unit_type,
position=position[0],
@ -715,7 +715,7 @@ class GroundConflictGenerator:
if is_player
else int(heading_sum(heading, 90))
)
country = self.game.player_country if is_player else self.game.enemy_country
country = self.game.coalition_for(is_player).country_name
for group in groups:
if group.role == CombatGroupRole.ARTILLERY:
distance_from_frontline = (

View File

@ -24,12 +24,13 @@ class CargoShipGenerator:
def generate(self) -> None:
# Reset the count to make generation deterministic.
for ship in self.game.transfers.cargo_ships:
self.generate_cargo_ship(ship)
for coalition in self.game.coalitions:
for ship in coalition.transfers.cargo_ships:
self.generate_cargo_ship(ship)
def generate_cargo_ship(self, ship: CargoShip) -> ShipGroup:
country = self.mission.country(
self.game.player_country if ship.player_owned else self.game.enemy_country
self.game.coalition_for(ship.player_owned).country_name
)
waypoints = ship.route
group = self.mission.ship_group(

View File

@ -27,8 +27,9 @@ class ConvoyGenerator:
def generate(self) -> None:
# Reset the count to make generation deterministic.
for convoy in self.game.transfers.convoys:
self.generate_convoy(convoy)
for coalition in self.game.coalitions:
for convoy in coalition.transfers.convoys:
self.generate_convoy(convoy)
def generate_convoy(self, convoy: Convoy) -> VehicleGroup:
group = self._create_mixed_unit_group(
@ -53,9 +54,7 @@ class ConvoyGenerator:
units: dict[GroundUnitType, int],
for_player: bool,
) -> VehicleGroup:
country = self.mission.country(
self.game.player_country if for_player else self.game.enemy_country
)
country = self.mission.country(self.game.coalition_for(for_player).country_name)
unit_types = list(units.items())
main_unit_type, main_unit_count = unit_types[0]

View File

@ -228,7 +228,7 @@ class CoalitionMissionPlanner:
self.game = game
self.is_player = is_player
self.objective_finder = ObjectiveFinder(self.game, self.is_player)
self.ato = self.game.blue_ato if is_player else self.game.red_ato
self.ato = self.game.coalition_for(is_player).ato
self.threat_zones = self.game.threat_zone_for(not self.is_player)
self.procurement_requests = self.game.procurement_requests_for(self.is_player)
self.faction: Faction = self.game.faction_for(self.is_player)

View File

@ -38,8 +38,8 @@ class ForcedOptionsGenerator:
self.mission.forced_options.labels = ForcedOptions.Labels.None_
def _set_unrestricted_satnav(self) -> None:
blue = self.game.player_faction
red = self.game.enemy_faction
blue = self.game.blue.faction
red = self.game.red.faction
if blue.unrestricted_satnav or red.unrestricted_satnav:
self.mission.forced_options.unrestricted_satnav = True

View File

@ -25,7 +25,7 @@ from typing import (
from dcs import Mission, Point, unitgroup
from dcs.action import SceneryDestructionZone
from dcs.country import Country
from dcs.point import StaticPoint, MovingPoint
from dcs.point import StaticPoint
from dcs.statics import Fortification, fortification_map, warehouse_map
from dcs.task import (
ActivateBeaconCommand,
@ -36,8 +36,8 @@ from dcs.task import (
)
from dcs.triggers import TriggerStart, TriggerZone
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, UnitType, ShipType, VehicleType
from dcs.unitgroup import ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, ShipType, VehicleType
from dcs.vehicles import vehicle_map
from game import db
@ -587,13 +587,7 @@ class HelipadGenerator:
self.tacan_registry = tacan_registry
def generate(self) -> None:
if self.cp.captured:
country_name = self.game.player_country
else:
country_name = self.game.enemy_country
country = self.m.country(country_name)
country = self.m.country(self.game.coalition_for(self.cp.captured).country_name)
for i, helipad in enumerate(self.cp.helipads):
name = self.cp.name + "_helipad_" + str(i)
logging.info("Generating helipad : " + name)
@ -636,12 +630,7 @@ class GroundObjectsGenerator:
def generate(self) -> None:
for cp in self.game.theater.controlpoints:
if cp.captured:
country_name = self.game.player_country
else:
country_name = self.game.enemy_country
country = self.m.country(country_name)
country = self.m.country(self.game.coalition_for(cp.captured).country_name)
HelipadGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
).generate()

View File

@ -97,7 +97,7 @@ class VisualGenerator:
break
self.mission.static_group(
self.mission.country(self.game.enemy_country),
self.mission.country(self.game.red.country_name),
"",
_type=v,
position=pos,

View File

@ -12,11 +12,10 @@ from PySide2.QtCore import (
)
from PySide2.QtGui import QIcon
from game import db
from game.game import Game
from game.squadrons import Squadron, Pilot
from game.theater.missiontarget import MissionTarget
from game.transfers import TransferOrder
from game.transfers import TransferOrder, PendingTransfers
from gen.ato import AirTaskingOrder, Package
from gen.flights.flight import Flight, FlightType
from gen.flights.traveltime import TotEstimator
@ -281,9 +280,9 @@ class AtoModel(QAbstractListModel):
self.package_models.clear()
if self.game is not None:
if player:
self.ato = self.game.blue_ato
self.ato = self.game.blue.ato
else:
self.ato = self.game.red_ato
self.ato = self.game.red.ato
else:
self.ato = AirTaskingOrder()
self.endResetModel()
@ -316,8 +315,12 @@ class TransferModel(QAbstractListModel):
super().__init__()
self.game_model = game_model
@property
def transfers(self) -> PendingTransfers:
return self.game_model.game.coalition_for(player=True).transfers
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return self.game_model.game.transfers.pending_transfer_count
return self.transfers.pending_transfer_count
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if not index.isValid():
@ -345,7 +348,7 @@ class TransferModel(QAbstractListModel):
"""Updates the game with the new unit transfer."""
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
# TODO: Needs to regenerate base inventory tab.
self.game_model.game.transfers.new_transfer(transfer)
self.transfers.new_transfer(transfer)
self.endInsertRows()
def cancel_transfer_at_index(self, index: QModelIndex) -> None:
@ -354,15 +357,15 @@ class TransferModel(QAbstractListModel):
def cancel_transfer(self, transfer: TransferOrder) -> None:
"""Cancels the planned unit transfer at the given index."""
index = self.game_model.game.transfers.index_of_transfer(transfer)
index = self.transfers.index_of_transfer(transfer)
self.beginRemoveRows(QModelIndex(), index, index)
# TODO: Needs to regenerate base inventory tab.
self.game_model.game.transfers.cancel_transfer(transfer)
self.transfers.cancel_transfer(transfer)
self.endRemoveRows()
def transfer_at_index(self, index: QModelIndex) -> TransferOrder:
"""Returns the transfer located at the given index."""
return self.game_model.game.transfers.transfer_at_index(index.row())
return self.transfers.transfer_at_index(index.row())
class AirWingModel(QAbstractListModel):
@ -488,8 +491,8 @@ class GameModel:
self.ato_model = AtoModel(self, AirTaskingOrder())
self.red_ato_model = AtoModel(self, AirTaskingOrder())
else:
self.ato_model = AtoModel(self, self.game.blue_ato)
self.red_ato_model = AtoModel(self, self.game.red_ato)
self.ato_model = AtoModel(self, self.game.blue.ato)
self.red_ato_model = AtoModel(self, self.game.red.ato)
def ato_model_for(self, player: bool) -> AtoModel:
if player:

View File

@ -24,8 +24,8 @@ class QFactionsInfos(QGroupBox):
def setGame(self, game: Game):
if game is not None:
self.player_name.setText(game.player_faction.name)
self.enemy_name.setText(game.enemy_faction.name)
self.player_name.setText(game.blue.faction.name)
self.enemy_name.setText(game.red.faction.name)
else:
self.player_name.setText("")
self.enemy_name.setText("")

View File

@ -168,7 +168,7 @@ class QTopPanel(QFrame):
package.time_over_target = estimator.earliest_tot()
def ato_has_clients(self) -> bool:
for package in self.game.blue_ato.packages:
for package in self.game.blue.ato.packages:
for flight in package.flights:
if flight.client_count > 0:
return True
@ -236,7 +236,7 @@ class QTopPanel(QFrame):
def check_no_missing_pilots(self) -> bool:
missing_pilots = []
for package in self.game.blue_ato.packages:
for package in self.game.blue.ato.packages:
for flight in package.flights:
if flight.missing_pilots > 0:
missing_pilots.append((package, flight))
@ -282,8 +282,8 @@ class QTopPanel(QFrame):
closest_cps[0],
closest_cps[1],
self.game.theater.controlpoints[0].position,
self.game.player_faction.name,
self.game.enemy_faction.name,
self.game.blue.faction.name,
self.game.red.faction.name,
)
unit_map = self.game.initiate_event(game_event)

View File

@ -4,7 +4,6 @@ from typing import Iterable, Type
from PySide2.QtWidgets import QComboBox
from dcs.unittype import FlyingType
from game import db
from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.flight import FlightType
@ -13,16 +12,12 @@ class QAircraftTypeSelector(QComboBox):
"""Combo box for selecting among the given aircraft types."""
def __init__(
self,
aircraft_types: Iterable[Type[FlyingType]],
country: str,
mission_type: FlightType,
self, aircraft_types: Iterable[Type[FlyingType]], mission_type: FlightType
) -> None:
super().__init__()
self.model().sort(0)
self.setSizeAdjustPolicy(self.AdjustToContents)
self.country = country
self.update_items(mission_type, aircraft_types)
def update_items(self, mission_type: FlightType, aircraft_types):

View File

@ -336,8 +336,12 @@ class SupplyRouteJs(QObject):
def find_transports(self) -> List[MultiGroupTransport]:
if self.sea_route:
return self.find_in_transport_map(self.game.transfers.cargo_ships)
return self.find_in_transport_map(self.game.transfers.convoys)
return self.find_in_transport_map(
self.game.blue.transfers.cargo_ships
) + self.find_in_transport_map(self.game.red.transfers.cargo_ships)
return self.find_in_transport_map(
self.game.blue.transfers.convoys
) + self.find_in_transport_map(self.game.red.transfers.convoys)
@Property(list, notify=activeTransportsChanged)
def activeTransports(self) -> List[str]:
@ -672,8 +676,8 @@ class NavMeshJs(QObject):
@classmethod
def from_game(cls, game: Game) -> NavMeshJs:
return NavMeshJs(
cls.to_polys(game.blue_navmesh, game.theater),
cls.to_polys(game.red_navmesh, game.theater),
cls.to_polys(game.blue.nav_mesh, game.theater),
cls.to_polys(game.red.nav_mesh, game.theater),
)
@ -870,8 +874,8 @@ class MapModel(QObject):
def reset_atos(self) -> None:
self._flights = self._flights_in_ato(
self.game.blue_ato, blue=True
) + self._flights_in_ato(self.game.red_ato, blue=False)
self.game.blue.ato, blue=True
) + self._flights_in_ato(self.game.red.ato, blue=False)
self.flightsChanged.emit()
@Property(list, notify=flightsChanged)

View File

@ -3,12 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Iterator
from PySide2.QtCore import (
QItemSelectionModel,
QModelIndex,
Qt,
QSize,
)
from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize
from PySide2.QtWidgets import (
QAbstractItemView,
QCheckBox,
@ -183,7 +178,7 @@ class AirInventoryView(QWidget):
self.table.setSortingEnabled(True)
def iter_allocated_aircraft(self) -> Iterator[AircraftInventoryData]:
for package in self.game_model.game.blue_ato.packages:
for package in self.game_model.game.blue.ato.packages:
for flight in package.flights:
yield from AircraftInventoryData.from_flight(flight)

View File

@ -73,11 +73,15 @@ class DepartingConvoysList(QFrame):
task_box_layout = QGridLayout()
scroll_content.setLayout(task_box_layout)
for convoy in game_model.game.transfers.convoys.departing_from(cp):
for convoy in game_model.game.coalition_for(
cp.captured
).transfers.convoys.departing_from(cp):
group_info = DepartingConvoyInfo(convoy)
task_box_layout.addWidget(group_info)
for cargo_ship in game_model.game.transfers.cargo_ships.departing_from(cp):
for cargo_ship in game_model.game.coalition_for(
cp.captured
).transfers.cargo_ships.departing_from(cp):
group_info = DepartingConvoyInfo(cargo_ship)
task_box_layout.addWidget(group_info)

View File

@ -195,7 +195,9 @@ class QBaseMenu2(QDialog):
ground_unit_limit = self.cp.frontline_unit_count_limit
deployable_unit_info = ""
allocated = self.cp.allocated_ground_units(self.game_model.game.transfers)
allocated = self.cp.allocated_ground_units(
self.game_model.game.coalition_for(self.cp.captured).transfers
)
unit_overage = max(
allocated.total_present - self.cp.frontline_unit_count_limit, 0
)

View File

@ -45,7 +45,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
row = 0
unit_types: Set[AircraftType] = set()
for unit_type in self.game_model.game.player_faction.aircrafts:
for unit_type in self.game_model.game.blue.faction.aircrafts:
if self.cp.is_carrier and not unit_type.carrier_capable:
continue
if self.cp.is_lha and not unit_type.lha_capable:

View File

@ -1,7 +1,6 @@
import logging
from typing import List, Optional
from PySide2 import QtCore
from PySide2.QtGui import Qt
from PySide2.QtWidgets import (
QComboBox,
@ -307,7 +306,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.buySamBox = QGroupBox("Buy SAM site :")
self.buyArmorBox = QGroupBox("Buy defensive position :")
faction = self.game.player_faction
faction = self.game.blue.faction
# Sams

View File

@ -38,7 +38,7 @@ class QFlightCreator(QDialog):
self.game = game
self.package = package
self.custom_name_text = None
self.country = self.game.player_country
self.country = self.game.blue.country_name
self.setWindowTitle("Create flight")
self.setWindowIcon(EVENT_ICONS["strike"])
@ -52,7 +52,6 @@ class QFlightCreator(QDialog):
self.aircraft_selector = QAircraftTypeSelector(
self.game.aircraft_inventory.available_types_for_player,
self.game.player_country,
self.task_selector.currentData(),
)
self.aircraft_selector.setCurrentIndex(0)