mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge pull request #147 from DanAlbert/ato
Replace mission planning UI.
This commit is contained in:
commit
6e14ec3227
@ -1142,7 +1142,7 @@ ShipDict = typing.Dict[ShipType, int]
|
||||
AirDefenseDict = typing.Dict[AirDefence, int]
|
||||
|
||||
AssignedUnitsDict = typing.Dict[typing.Type[UnitType], typing.Tuple[int, int]]
|
||||
TaskForceDict = typing.Dict[typing.Type[Task], AssignedUnitsDict]
|
||||
TaskForceDict = typing.Dict[typing.Type[MainTask], AssignedUnitsDict]
|
||||
|
||||
StartingPosition = typing.Optional[typing.Union[ShipGroup, StaticGroup, Airport, Point]]
|
||||
|
||||
|
||||
15
game/game.py
15
game/game.py
@ -1,7 +1,9 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from game.db import REWARDS, PLAYER_BUDGET_BASE, sys
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from game.models.game_stats import GameStats
|
||||
from gen.ato import AirTaskingOrder
|
||||
from gen.flights.ai_flight_planner import FlightPlanner
|
||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from .event import *
|
||||
@ -78,6 +80,13 @@ class Game:
|
||||
self.jtacs = []
|
||||
self.savepath = ""
|
||||
|
||||
self.blue_ato = AirTaskingOrder()
|
||||
self.red_ato = AirTaskingOrder()
|
||||
|
||||
self.aircraft_inventory = GlobalAircraftInventory(
|
||||
self.theater.controlpoints
|
||||
)
|
||||
|
||||
self.sanitize_sides()
|
||||
|
||||
|
||||
@ -229,10 +238,16 @@ class Game:
|
||||
# Update statistics
|
||||
self.game_stats.update(self)
|
||||
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.__culling_points = self.compute_conflicts_position()
|
||||
self.planners = {}
|
||||
self.ground_planners = {}
|
||||
self.blue_ato.clear()
|
||||
self.red_ato.clear()
|
||||
for cp in self.theater.controlpoints:
|
||||
if cp.has_runway():
|
||||
planner = FlightPlanner(cp, self)
|
||||
|
||||
129
game/inventory.py
Normal file
129
game/inventory.py
Normal file
@ -0,0 +1,129 @@
|
||||
"""Inventory management APIs."""
|
||||
from collections import defaultdict
|
||||
from typing import Dict, Iterable, Iterator, Set, Tuple
|
||||
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
class ControlPointAircraftInventory:
|
||||
"""Aircraft inventory for a single control point."""
|
||||
|
||||
def __init__(self, control_point: "ControlPoint") -> None:
|
||||
self.control_point = control_point
|
||||
self.inventory: Dict[UnitType, int] = defaultdict(int)
|
||||
|
||||
def add_aircraft(self, aircraft: UnitType, count: int) -> None:
|
||||
"""Adds aircraft to the inventory.
|
||||
|
||||
Args:
|
||||
aircraft: The type of aircraft to add.
|
||||
count: The number of aircraft to add.
|
||||
"""
|
||||
self.inventory[aircraft] += count
|
||||
|
||||
def remove_aircraft(self, aircraft: UnitType, count: int) -> None:
|
||||
"""Removes aircraft from the inventory.
|
||||
|
||||
Args:
|
||||
aircraft: The type of aircraft to remove.
|
||||
count: The number of aircraft to remove.
|
||||
|
||||
Raises:
|
||||
ValueError: The control point cannot fulfill the requested number of
|
||||
aircraft.
|
||||
"""
|
||||
available = self.inventory[aircraft]
|
||||
if available < count:
|
||||
raise ValueError(
|
||||
f"Cannot remove {count} {aircraft.id} from "
|
||||
f"{self.control_point.name}. Only have {available}."
|
||||
)
|
||||
self.inventory[aircraft] -= count
|
||||
|
||||
def available(self, aircraft: UnitType) -> int:
|
||||
"""Returns the number of available aircraft of the given type.
|
||||
|
||||
Args:
|
||||
aircraft: The type of aircraft to query.
|
||||
"""
|
||||
return self.inventory[aircraft]
|
||||
|
||||
@property
|
||||
def types_available(self) -> Iterator[UnitType]:
|
||||
"""Iterates over all available aircraft types."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
yield aircraft
|
||||
|
||||
@property
|
||||
def all_aircraft(self) -> Iterator[Tuple[UnitType, int]]:
|
||||
"""Iterates over all available aircraft types, including amounts."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
yield aircraft, count
|
||||
|
||||
@property
|
||||
def total_available(self) -> int:
|
||||
"""Returns the total number of aircraft available."""
|
||||
# TODO: Remove?
|
||||
# This probably isn't actually useful. It's used by the AI flight
|
||||
# planner to determine how many flights of a given type it should
|
||||
# allocate, but it should probably be making that decision based on the
|
||||
# number of aircraft available to perform a particular role.
|
||||
return sum(self.inventory.values())
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clears all aircraft from the inventory."""
|
||||
self.inventory.clear()
|
||||
|
||||
|
||||
class GlobalAircraftInventory:
|
||||
"""Game-wide aircraft inventory."""
|
||||
def __init__(self, control_points: Iterable["ControlPoint"]) -> None:
|
||||
self.inventories: Dict["ControlPoint", ControlPointAircraftInventory] = {
|
||||
cp: ControlPointAircraftInventory(cp) for cp in control_points
|
||||
}
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Clears all control points and their inventories."""
|
||||
for inventory in self.inventories.values():
|
||||
inventory.clear()
|
||||
|
||||
def set_from_control_point(self, control_point: "ControlPoint") -> None:
|
||||
"""Set the control point's aircraft inventory.
|
||||
|
||||
If the inventory for the given control point has already been set for
|
||||
the turn, it will be overwritten.
|
||||
"""
|
||||
inventory = self.inventories[control_point]
|
||||
for aircraft, count in control_point.base.aircraft.items():
|
||||
inventory.add_aircraft(aircraft, count)
|
||||
|
||||
def for_control_point(
|
||||
self,
|
||||
control_point: "ControlPoint") -> ControlPointAircraftInventory:
|
||||
"""Returns the inventory specific to the given control point."""
|
||||
return self.inventories[control_point]
|
||||
|
||||
@property
|
||||
def available_types_for_player(self) -> Iterator[UnitType]:
|
||||
"""Iterates over all aircraft types available to the player."""
|
||||
seen: Set[UnitType] = set()
|
||||
for control_point, inventory in self.inventories.items():
|
||||
if control_point.captured:
|
||||
for aircraft in inventory.types_available:
|
||||
if aircraft not in seen:
|
||||
seen.add(aircraft)
|
||||
yield aircraft
|
||||
|
||||
def claim_for_flight(self, flight: Flight) -> None:
|
||||
"""Removes aircraft from the inventory for the given flight."""
|
||||
inventory = self.for_control_point(flight.from_cp)
|
||||
inventory.remove_aircraft(flight.unit_type, flight.count)
|
||||
|
||||
def return_from_flight(self, flight: Flight) -> None:
|
||||
"""Returns a flight's aircraft to the inventory."""
|
||||
inventory = self.for_control_point(flight.from_cp)
|
||||
inventory.add_aircraft(flight.unit_type, flight.count)
|
||||
117
gen/ato.py
Normal file
117
gen/ato.py
Normal file
@ -0,0 +1,117 @@
|
||||
"""Air Tasking Orders.
|
||||
|
||||
The classes of the Air Tasking Order (ATO) define all of the missions that have
|
||||
been planned, and which aircraft have been assigned to them. Each planned
|
||||
mission, or "package" is composed of individual flights. The package may contain
|
||||
dissimilar aircraft performing different roles, but all for the same goal. For
|
||||
example, the package to strike an enemy airfield may contain an escort flight,
|
||||
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
|
||||
the single CAP flight.
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
|
||||
from .flights.flight import Flight, FlightType
|
||||
from theater.missiontarget import MissionTarget
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Task:
|
||||
"""The main task of a flight or package."""
|
||||
|
||||
#: The type of task.
|
||||
task_type: FlightType
|
||||
|
||||
#: The location of the objective.
|
||||
location: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Package:
|
||||
"""A mission package."""
|
||||
|
||||
#: The mission target. Currently can be either a ControlPoint or a
|
||||
#: TheaterGroundObject (non-ControlPoint map objectives).
|
||||
target: MissionTarget
|
||||
|
||||
#: The set of flights in the package.
|
||||
flights: List[Flight] = field(default_factory=list)
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds a flight to the package."""
|
||||
self.flights.append(flight)
|
||||
|
||||
def remove_flight(self, flight: Flight) -> None:
|
||||
"""Removes a flight from the package."""
|
||||
self.flights.remove(flight)
|
||||
|
||||
@property
|
||||
def package_description(self) -> str:
|
||||
"""Generates a package description based on flight composition."""
|
||||
if not self.flights:
|
||||
return "No mission"
|
||||
|
||||
flight_counts: Dict[FlightType, int] = defaultdict(lambda: 0)
|
||||
for flight in self.flights:
|
||||
flight_counts[flight.flight_type] += 1
|
||||
|
||||
# The package will contain a mix of mission types, but in general we can
|
||||
# determine the goal of the mission because some mission types are more
|
||||
# likely to be the main task than others. For example, a package with
|
||||
# only CAP flights is a CAP package, a flight with CAP and strike is a
|
||||
# strike package, a flight with CAP and DEAD is a DEAD package, and a
|
||||
# flight with strike and SEAD is an OCA/Strike package. The type of
|
||||
# package is determined by the highest priority flight in the package.
|
||||
task_priorities = [
|
||||
FlightType.CAS,
|
||||
FlightType.STRIKE,
|
||||
FlightType.ANTISHIP,
|
||||
FlightType.BAI,
|
||||
FlightType.EVAC,
|
||||
FlightType.TROOP_TRANSPORT,
|
||||
FlightType.RECON,
|
||||
FlightType.ELINT,
|
||||
FlightType.DEAD,
|
||||
FlightType.SEAD,
|
||||
FlightType.LOGISTICS,
|
||||
FlightType.INTERCEPTION,
|
||||
FlightType.TARCAP,
|
||||
FlightType.CAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.EWAR,
|
||||
]
|
||||
for task in task_priorities:
|
||||
if flight_counts[task]:
|
||||
return task.name
|
||||
|
||||
# If we get here, our task_priorities list above is incomplete. Log the
|
||||
# issue and return the type of *any* flight in the package.
|
||||
some_mission = next(iter(self.flights)).flight_type
|
||||
logging.warning(f"Unhandled mission type: {some_mission}")
|
||||
return some_mission.name
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# TODO: Far from perfect. Number packages?
|
||||
return hash(self.target.name)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirTaskingOrder:
|
||||
"""The entire ATO for one coalition."""
|
||||
|
||||
#: The set of all planned packages in the ATO.
|
||||
packages: List[Package] = field(default_factory=list)
|
||||
|
||||
def add_package(self, package: Package) -> None:
|
||||
"""Adds a package to the ATO."""
|
||||
self.packages.append(package)
|
||||
|
||||
def remove_package(self, package: Package) -> None:
|
||||
"""Removes a package from the ATO."""
|
||||
self.packages.remove(package)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Removes all packages from the ATO."""
|
||||
self.packages.clear()
|
||||
@ -1,28 +1,47 @@
|
||||
import math
|
||||
import operator
|
||||
import random
|
||||
from typing import Iterable, Iterator, List, Tuple
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.data.doctrine import MODERN_DOCTRINE
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.utils import meter_to_feet, nm_to_meter
|
||||
from game.utils import nm_to_meter
|
||||
from gen import Conflict
|
||||
from gen.flights.ai_flight_planner_db import INTERCEPT_CAPABLE, CAP_CAPABLE, CAS_CAPABLE, SEAD_CAPABLE, STRIKE_CAPABLE, \
|
||||
DRONES
|
||||
from gen.flights.flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
CAP_CAPABLE,
|
||||
CAS_CAPABLE,
|
||||
DRONES,
|
||||
SEAD_CAPABLE,
|
||||
STRIKE_CAPABLE,
|
||||
)
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
FlightType,
|
||||
FlightWaypoint,
|
||||
FlightWaypointType,
|
||||
)
|
||||
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
|
||||
|
||||
MISSION_DURATION = 80
|
||||
|
||||
|
||||
# TODO: Should not be per-control point.
|
||||
# Packages can frag flights from individual airfields, so we should be planning
|
||||
# coalition wide rather than per airfield.
|
||||
class FlightPlanner:
|
||||
|
||||
def __init__(self, from_cp, game):
|
||||
def __init__(self, from_cp: ControlPoint, game: "Game") -> None:
|
||||
# TODO : have the flight planner depend on a 'stance' setting : [Defensive, Aggresive... etc] and faction doctrine
|
||||
# TODO : the flight planner should plan package and operations
|
||||
self.from_cp = from_cp
|
||||
self.game = game
|
||||
self.aircraft_inventory = {} # local copy of the airbase inventory
|
||||
self.flights: List[Flight] = []
|
||||
self.potential_sead_targets: List[Tuple[TheaterGroundObject, int]] = []
|
||||
self.potential_strike_targets: List[Tuple[TheaterGroundObject, int]] = []
|
||||
|
||||
if from_cp.captured:
|
||||
self.faction = self.game.player_faction
|
||||
@ -34,249 +53,155 @@ class FlightPlanner:
|
||||
else:
|
||||
self.doctrine = MODERN_DOCTRINE
|
||||
|
||||
@property
|
||||
def aircraft_inventory(self) -> "GlobalAircraftInventory":
|
||||
return self.game.aircraft_inventory
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset the planned flights and available units
|
||||
"""
|
||||
self.aircraft_inventory = dict({k: v for k, v in self.from_cp.base.aircraft.items()})
|
||||
self.interceptor_flights = []
|
||||
self.cap_flights = []
|
||||
self.cas_flights = []
|
||||
self.strike_flights = []
|
||||
self.sead_flights = []
|
||||
self.custom_flights = []
|
||||
def reset(self) -> None:
|
||||
"""Reset the planned flights and available units."""
|
||||
self.flights = []
|
||||
self.potential_sead_targets = []
|
||||
self.potential_strike_targets = []
|
||||
|
||||
def plan_flights(self):
|
||||
|
||||
def plan_flights(self) -> None:
|
||||
self.reset()
|
||||
self.compute_sead_targets()
|
||||
self.compute_strike_targets()
|
||||
|
||||
# The priority is to assign air-superiority fighter or interceptor to interception roles, so they can scramble if there is an attacker
|
||||
# self.commision_interceptors()
|
||||
self.commission_cap()
|
||||
self.commission_cas()
|
||||
self.commission_sead()
|
||||
self.commission_strike()
|
||||
# TODO: Commission anti-ship and intercept.
|
||||
|
||||
# Then some CAP patrol for the next 2 hours
|
||||
self.commision_cap()
|
||||
def plan_legacy_mission(self, flight: Flight,
|
||||
location: MissionTarget) -> None:
|
||||
package = Package(location)
|
||||
package.add_flight(flight)
|
||||
if flight.from_cp.captured:
|
||||
self.game.blue_ato.add_package(package)
|
||||
else:
|
||||
self.game.red_ato.add_package(package)
|
||||
self.flights.append(flight)
|
||||
self.aircraft_inventory.claim_for_flight(flight)
|
||||
|
||||
# Then setup cas
|
||||
self.commision_cas()
|
||||
def get_compatible_aircraft(self, candidates: Iterable[FlyingType],
|
||||
minimum: int) -> List[FlyingType]:
|
||||
inventory = self.aircraft_inventory.for_control_point(self.from_cp)
|
||||
return [k for k, v in inventory.all_aircraft if
|
||||
k in candidates and v >= minimum]
|
||||
|
||||
# Then prepare some sead flights if required
|
||||
self.commision_sead()
|
||||
|
||||
self.commision_strike()
|
||||
|
||||
# TODO : commision ANTISHIP
|
||||
|
||||
def remove_flight(self, index):
|
||||
try:
|
||||
flight = self.flights[index]
|
||||
if flight in self.interceptor_flights: self.interceptor_flights.remove(flight)
|
||||
if flight in self.cap_flights: self.cap_flights.remove(flight)
|
||||
if flight in self.cas_flights: self.cas_flights.remove(flight)
|
||||
if flight in self.strike_flights: self.strike_flights.remove(flight)
|
||||
if flight in self.sead_flights: self.sead_flights.remove(flight)
|
||||
if flight in self.custom_flights: self.custom_flights.remove(flight)
|
||||
self.flights.remove(flight)
|
||||
except IndexError:
|
||||
def alloc_aircraft(
|
||||
self, num_flights: int, flight_size: int,
|
||||
allowed_types: Iterable[FlyingType]) -> Iterator[FlyingType]:
|
||||
aircraft = self.get_compatible_aircraft(allowed_types, flight_size)
|
||||
if not aircraft:
|
||||
return
|
||||
|
||||
for _ in range(num_flights):
|
||||
yield random.choice(aircraft)
|
||||
aircraft = self.get_compatible_aircraft(allowed_types, flight_size)
|
||||
if not aircraft:
|
||||
return
|
||||
|
||||
def commision_interceptors(self):
|
||||
"""
|
||||
Pick some aircraft to assign them to interception roles
|
||||
"""
|
||||
def commission_cap(self) -> None:
|
||||
"""Pick some aircraft to assign them to defensive CAP roles (BARCAP)."""
|
||||
offset = random.randint(0, 5)
|
||||
num_caps = MISSION_DURATION // self.doctrine["CAP_EVERY_X_MINUTES"]
|
||||
for i, aircraft in enumerate(self.alloc_aircraft(num_caps, 2, CAP_CAPABLE)):
|
||||
flight = Flight(aircraft, 2, self.from_cp, FlightType.CAP)
|
||||
|
||||
# At least try to generate one interceptor group
|
||||
number_of_interceptor_groups = min(max(sum([v for k, v in self.aircraft_inventory.items()]) / 4, self.doctrine["MAX_NUMBER_OF_INTERCEPTION_GROUP"]), 1)
|
||||
possible_interceptors = [k for k in self.aircraft_inventory.keys() if k in INTERCEPT_CAPABLE]
|
||||
|
||||
if len(possible_interceptors) <= 0:
|
||||
possible_interceptors = [k for k,v in self.aircraft_inventory.items() if k in CAP_CAPABLE and v >= 2]
|
||||
|
||||
if number_of_interceptor_groups > 0:
|
||||
inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_interceptors})
|
||||
for i in range(number_of_interceptor_groups):
|
||||
try:
|
||||
unit = random.choice([k for k,v in inventory.items() if v >= 2])
|
||||
except IndexError:
|
||||
break
|
||||
inventory[unit] = inventory[unit] - 2
|
||||
flight = Flight(unit, 2, self.from_cp, FlightType.INTERCEPTION)
|
||||
flight.scheduled_in = 1
|
||||
flight.points = []
|
||||
|
||||
self.interceptor_flights.append(flight)
|
||||
self.flights.append(flight)
|
||||
|
||||
# Update inventory
|
||||
for k, v in inventory.items():
|
||||
self.aircraft_inventory[k] = v
|
||||
|
||||
def commision_cap(self):
|
||||
"""
|
||||
Pick some aircraft to assign them to defensive CAP roles (BARCAP)
|
||||
"""
|
||||
|
||||
possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in CAP_CAPABLE and v >= 2]
|
||||
inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft})
|
||||
|
||||
offset = random.randint(0,5)
|
||||
for i in range(int(MISSION_DURATION/self.doctrine["CAP_EVERY_X_MINUTES"])):
|
||||
|
||||
try:
|
||||
unit = random.choice([k for k, v in inventory.items() if v >= 2])
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
inventory[unit] = inventory[unit] - 2
|
||||
flight = Flight(unit, 2, self.from_cp, FlightType.CAP)
|
||||
|
||||
flight.points = []
|
||||
flight.scheduled_in = offset + i*random.randint(self.doctrine["CAP_EVERY_X_MINUTES"] - 5, self.doctrine["CAP_EVERY_X_MINUTES"] + 5)
|
||||
flight.scheduled_in = offset + i * random.randint(
|
||||
self.doctrine["CAP_EVERY_X_MINUTES"] - 5,
|
||||
self.doctrine["CAP_EVERY_X_MINUTES"] + 5
|
||||
)
|
||||
|
||||
if len(self._get_cas_locations()) > 0:
|
||||
enemy_cp = random.choice(self._get_cas_locations())
|
||||
self.generate_frontline_cap(flight, flight.from_cp, enemy_cp)
|
||||
location = random.choice(self._get_cas_locations())
|
||||
self.generate_frontline_cap(flight, location)
|
||||
else:
|
||||
location = flight.from_cp
|
||||
self.generate_barcap(flight, flight.from_cp)
|
||||
|
||||
self.cap_flights.append(flight)
|
||||
self.flights.append(flight)
|
||||
self.plan_legacy_mission(flight, location)
|
||||
|
||||
# Update inventory
|
||||
for k, v in inventory.items():
|
||||
self.aircraft_inventory[k] = v
|
||||
def commission_cas(self) -> None:
|
||||
"""Pick some aircraft to assign them to CAS."""
|
||||
cas_locations = self._get_cas_locations()
|
||||
if not cas_locations:
|
||||
return
|
||||
|
||||
def commision_cas(self):
|
||||
"""
|
||||
Pick some aircraft to assign them to CAS
|
||||
"""
|
||||
offset = random.randint(0,5)
|
||||
num_cas = MISSION_DURATION // self.doctrine["CAS_EVERY_X_MINUTES"]
|
||||
for i, aircraft in enumerate(self.alloc_aircraft(num_cas, 2, CAS_CAPABLE)):
|
||||
flight = Flight(aircraft, 2, self.from_cp, FlightType.CAS)
|
||||
flight.scheduled_in = offset + i * random.randint(
|
||||
self.doctrine["CAS_EVERY_X_MINUTES"] - 5,
|
||||
self.doctrine["CAS_EVERY_X_MINUTES"] + 5)
|
||||
location = random.choice(cas_locations)
|
||||
|
||||
possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in CAS_CAPABLE and v >= 2]
|
||||
inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft})
|
||||
cas_location = self._get_cas_locations()
|
||||
self.generate_cas(flight, location)
|
||||
self.plan_legacy_mission(flight, location)
|
||||
|
||||
if len(cas_location) > 0:
|
||||
def commission_sead(self) -> None:
|
||||
"""Pick some aircraft to assign them to SEAD tasks."""
|
||||
|
||||
offset = random.randint(0,5)
|
||||
for i in range(int(MISSION_DURATION/self.doctrine["CAS_EVERY_X_MINUTES"])):
|
||||
if not self.potential_sead_targets:
|
||||
return
|
||||
|
||||
try:
|
||||
unit = random.choice([k for k, v in inventory.items() if v >= 2])
|
||||
except IndexError:
|
||||
break
|
||||
offset = random.randint(0, 5)
|
||||
num_sead = max(
|
||||
MISSION_DURATION // self.doctrine["SEAD_EVERY_X_MINUTES"],
|
||||
len(self.potential_sead_targets))
|
||||
for i, aircraft in enumerate(self.alloc_aircraft(num_sead, 2, SEAD_CAPABLE)):
|
||||
flight = Flight(aircraft, 2, self.from_cp,
|
||||
random.choice([FlightType.SEAD, FlightType.DEAD]))
|
||||
flight.scheduled_in = offset + i * random.randint(
|
||||
self.doctrine["SEAD_EVERY_X_MINUTES"] - 5,
|
||||
self.doctrine["SEAD_EVERY_X_MINUTES"] + 5)
|
||||
|
||||
inventory[unit] = inventory[unit] - 2
|
||||
flight = Flight(unit, 2, self.from_cp, FlightType.CAS)
|
||||
flight.points = []
|
||||
flight.scheduled_in = offset + i * random.randint(self.doctrine["CAS_EVERY_X_MINUTES"] - 5, self.doctrine["CAS_EVERY_X_MINUTES"] + 5)
|
||||
location = random.choice(cas_location)
|
||||
location = self.potential_sead_targets[0][0]
|
||||
self.potential_sead_targets.pop()
|
||||
|
||||
self.generate_cas(flight, flight.from_cp, location)
|
||||
self.generate_sead(flight, location, [])
|
||||
self.plan_legacy_mission(flight, location)
|
||||
|
||||
self.cas_flights.append(flight)
|
||||
self.flights.append(flight)
|
||||
def commission_strike(self) -> None:
|
||||
"""Pick some aircraft to assign them to STRIKE tasks."""
|
||||
if not self.potential_strike_targets:
|
||||
return
|
||||
|
||||
# Update inventory
|
||||
for k, v in inventory.items():
|
||||
self.aircraft_inventory[k] = v
|
||||
offset = random.randint(0,5)
|
||||
num_strike = max(
|
||||
MISSION_DURATION / self.doctrine["STRIKE_EVERY_X_MINUTES"],
|
||||
len(self.potential_strike_targets)
|
||||
)
|
||||
for i, aircraft in enumerate(self.alloc_aircraft(num_strike, 2, STRIKE_CAPABLE)):
|
||||
if aircraft in DRONES:
|
||||
count = 1
|
||||
else:
|
||||
count = 2
|
||||
|
||||
def commision_sead(self):
|
||||
"""
|
||||
Pick some aircraft to assign them to SEAD tasks
|
||||
"""
|
||||
flight = Flight(aircraft, count, self.from_cp, FlightType.STRIKE)
|
||||
flight.scheduled_in = offset + i * random.randint(
|
||||
self.doctrine["STRIKE_EVERY_X_MINUTES"] - 5,
|
||||
self.doctrine["STRIKE_EVERY_X_MINUTES"] + 5)
|
||||
|
||||
possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in SEAD_CAPABLE and v >= 2]
|
||||
inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft})
|
||||
location = self.potential_strike_targets[0][0]
|
||||
self.potential_strike_targets.pop(0)
|
||||
|
||||
if len(self.potential_sead_targets) > 0:
|
||||
self.generate_strike(flight, location)
|
||||
self.plan_legacy_mission(flight, location)
|
||||
|
||||
offset = random.randint(0,5)
|
||||
for i in range(int(MISSION_DURATION/self.doctrine["SEAD_EVERY_X_MINUTES"])):
|
||||
|
||||
if len(self.potential_sead_targets) <= 0:
|
||||
break
|
||||
|
||||
try:
|
||||
unit = random.choice([k for k, v in inventory.items() if v >= 2])
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
inventory[unit] = inventory[unit] - 2
|
||||
flight = Flight(unit, 2, self.from_cp, random.choice([FlightType.SEAD, FlightType.DEAD]))
|
||||
|
||||
flight.points = []
|
||||
flight.scheduled_in = offset + i*random.randint(self.doctrine["SEAD_EVERY_X_MINUTES"] - 5, self.doctrine["SEAD_EVERY_X_MINUTES"] + 5)
|
||||
|
||||
location = self.potential_sead_targets[0][0]
|
||||
self.potential_sead_targets.pop(0)
|
||||
|
||||
self.generate_sead(flight, location, [])
|
||||
|
||||
self.sead_flights.append(flight)
|
||||
self.flights.append(flight)
|
||||
|
||||
# Update inventory
|
||||
for k, v in inventory.items():
|
||||
self.aircraft_inventory[k] = v
|
||||
|
||||
|
||||
def commision_strike(self):
|
||||
"""
|
||||
Pick some aircraft to assign them to STRIKE tasks
|
||||
"""
|
||||
possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in STRIKE_CAPABLE and v >= 2]
|
||||
inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft})
|
||||
|
||||
if len(self.potential_strike_targets) > 0:
|
||||
|
||||
offset = random.randint(0,5)
|
||||
for i in range(int(MISSION_DURATION/self.doctrine["STRIKE_EVERY_X_MINUTES"])):
|
||||
|
||||
if len(self.potential_strike_targets) <= 0:
|
||||
break
|
||||
|
||||
try:
|
||||
unit = random.choice([k for k, v in inventory.items() if v >= 2])
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
if unit in DRONES:
|
||||
count = 1
|
||||
else:
|
||||
count = 2
|
||||
|
||||
inventory[unit] = inventory[unit] - count
|
||||
flight = Flight(unit, count, self.from_cp, FlightType.STRIKE)
|
||||
|
||||
flight.points = []
|
||||
flight.scheduled_in = offset + i*random.randint(self.doctrine["STRIKE_EVERY_X_MINUTES"] - 5, self.doctrine["STRIKE_EVERY_X_MINUTES"] + 5)
|
||||
|
||||
location = self.potential_strike_targets[0][0]
|
||||
self.potential_strike_targets.pop(0)
|
||||
|
||||
self.generate_strike(flight, location)
|
||||
|
||||
self.strike_flights.append(flight)
|
||||
self.flights.append(flight)
|
||||
|
||||
# Update inventory
|
||||
for k, v in inventory.items():
|
||||
self.aircraft_inventory[k] = v
|
||||
|
||||
def _get_cas_locations(self):
|
||||
def _get_cas_locations(self) -> List[FrontLine]:
|
||||
return self._get_cas_locations_for_cp(self.from_cp)
|
||||
|
||||
def _get_cas_locations_for_cp(self, for_cp):
|
||||
@staticmethod
|
||||
def _get_cas_locations_for_cp(for_cp: ControlPoint) -> List[FrontLine]:
|
||||
cas_locations = []
|
||||
for cp in for_cp.connected_points:
|
||||
if cp.captured != for_cp.captured:
|
||||
cas_locations.append(cp)
|
||||
cas_locations.append(FrontLine(for_cp, cp))
|
||||
return cas_locations
|
||||
|
||||
def compute_strike_targets(self):
|
||||
@ -351,18 +276,7 @@ class FlightPlanner:
|
||||
return "-"*40 + "\n" + self.from_cp.name + " planned flights :\n"\
|
||||
+ "-"*40 + "\n" + "\n".join([repr(f) for f in self.flights]) + "\n" + "-"*40
|
||||
|
||||
def get_available_aircraft(self):
|
||||
base_aircraft_inventory = dict({k: v for k, v in self.from_cp.base.aircraft.items()})
|
||||
for f in self.flights:
|
||||
if f.unit_type in base_aircraft_inventory.keys():
|
||||
base_aircraft_inventory[f.unit_type] = base_aircraft_inventory[f.unit_type] - f.count
|
||||
if base_aircraft_inventory[f.unit_type] <= 0:
|
||||
del base_aircraft_inventory[f.unit_type]
|
||||
return base_aircraft_inventory
|
||||
|
||||
|
||||
def generate_strike(self, flight, location):
|
||||
|
||||
def generate_strike(self, flight: Flight, location: TheaterGroundObject):
|
||||
flight.flight_type = FlightType.STRIKE
|
||||
ascend = self.generate_ascend_point(flight.from_cp)
|
||||
flight.points.append(ascend)
|
||||
@ -512,17 +426,17 @@ class FlightPlanner:
|
||||
rtb = self.generate_rtb_waypoint(flight.from_cp)
|
||||
flight.points.append(rtb)
|
||||
|
||||
def generate_frontline_cap(self, flight: Flight,
|
||||
front_line: FrontLine) -> None:
|
||||
"""Generate a CAP flight plan for the given front line.
|
||||
|
||||
def generate_frontline_cap(self, flight, ally_cp, enemy_cp):
|
||||
"""
|
||||
Generate a cap flight for the frontline between ally_cp and enemy cp in order to ensure air superiority and
|
||||
protect friendly CAP airbase
|
||||
:param flight: Flight to setup
|
||||
:param ally_cp: CP to protect
|
||||
:param enemy_cp: Enemy connected cp
|
||||
:param front_line: Front line to protect.
|
||||
"""
|
||||
ally_cp, enemy_cp = front_line.control_points
|
||||
flight.flight_type = FlightType.CAP
|
||||
patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0], self.doctrine["PATROL_ALT_RANGE"][1])
|
||||
patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0],
|
||||
self.doctrine["PATROL_ALT_RANGE"][1])
|
||||
|
||||
# Find targets waypoints
|
||||
ingress, heading, distance = Conflict.frontline_vector(ally_cp, enemy_cp, self.game.theater)
|
||||
@ -663,19 +577,21 @@ class FlightPlanner:
|
||||
rtb = self.generate_rtb_waypoint(flight.from_cp)
|
||||
flight.points.append(rtb)
|
||||
|
||||
def generate_cas(self, flight: Flight, front_line: FrontLine) -> None:
|
||||
"""Generate a CAS flight plan for the given target.
|
||||
|
||||
def generate_cas(self, flight, from_cp, location):
|
||||
"""
|
||||
Generate a CAS flight at a given location
|
||||
:param flight: Flight to setup
|
||||
:param location: Location of the CAS targets
|
||||
:param front_line: Front line containing CAS targets.
|
||||
"""
|
||||
from_cp, location = front_line.control_points
|
||||
is_helo = hasattr(flight.unit_type, "helicopter") and flight.unit_type.helicopter
|
||||
cap_alt = 1000
|
||||
flight.points = []
|
||||
flight.flight_type = FlightType.CAS
|
||||
|
||||
ingress, heading, distance = Conflict.frontline_vector(from_cp, location, self.game.theater)
|
||||
ingress, heading, distance = Conflict.frontline_vector(
|
||||
from_cp, location, self.game.theater
|
||||
)
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
egress = ingress.point_from_heading(heading, distance)
|
||||
|
||||
|
||||
60
qt_ui/dialogs.py
Normal file
60
qt_ui/dialogs.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Application-wide dialog management."""
|
||||
from typing import Optional
|
||||
|
||||
from gen.flights.flight import Flight
|
||||
from theater.missiontarget import MissionTarget
|
||||
from .models import GameModel, PackageModel
|
||||
from .windows.mission.QEditFlightDialog import QEditFlightDialog
|
||||
from .windows.mission.QPackageDialog import (
|
||||
QEditPackageDialog,
|
||||
QNewPackageDialog,
|
||||
)
|
||||
|
||||
|
||||
class Dialog:
|
||||
"""Dialog management singleton.
|
||||
|
||||
Opens dialogs and keeps references to dialog windows so that their creators
|
||||
do not need to worry about the lifetime of the dialog object, and can open
|
||||
dialogs without needing to have their own reference to common data like the
|
||||
game model.
|
||||
"""
|
||||
|
||||
#: The game model. Is only None before initialization, as the game model
|
||||
#: itself is responsible for handling the case where no game is loaded.
|
||||
game_model: Optional[GameModel] = None
|
||||
|
||||
new_package_dialog: Optional[QNewPackageDialog] = None
|
||||
edit_package_dialog: Optional[QEditPackageDialog] = None
|
||||
edit_flight_dialog: Optional[QEditFlightDialog] = None
|
||||
|
||||
@classmethod
|
||||
def set_game(cls, game_model: GameModel) -> None:
|
||||
"""Sets the game model."""
|
||||
cls.game_model = game_model
|
||||
|
||||
@classmethod
|
||||
def open_new_package_dialog(cls, mission_target: MissionTarget):
|
||||
"""Opens the dialog to create a new package with the given target."""
|
||||
cls.new_package_dialog = QNewPackageDialog(
|
||||
cls.game_model.game,
|
||||
cls.game_model.ato_model,
|
||||
mission_target
|
||||
)
|
||||
cls.new_package_dialog.show()
|
||||
|
||||
@classmethod
|
||||
def open_edit_package_dialog(cls, package_model: PackageModel):
|
||||
"""Opens the dialog to edit the given package."""
|
||||
cls.edit_package_dialog = QEditPackageDialog(
|
||||
cls.game_model.game,
|
||||
cls.game_model.ato_model,
|
||||
package_model
|
||||
)
|
||||
cls.edit_package_dialog.show()
|
||||
|
||||
@classmethod
|
||||
def open_edit_flight_dialog(cls, flight: Flight):
|
||||
"""Opens the dialog to edit the given flight."""
|
||||
cls.edit_flight_dialog = QEditFlightDialog(cls.game_model.game, flight)
|
||||
cls.edit_flight_dialog.show()
|
||||
268
qt_ui/models.py
Normal file
268
qt_ui/models.py
Normal file
@ -0,0 +1,268 @@
|
||||
"""Qt data models for game objects."""
|
||||
from typing import Any, Callable, Dict, Iterator, TypeVar, Optional
|
||||
|
||||
from PySide2.QtCore import (
|
||||
QAbstractListModel,
|
||||
QModelIndex,
|
||||
Qt,
|
||||
Signal,
|
||||
)
|
||||
from PySide2.QtGui import QIcon
|
||||
|
||||
from game import db
|
||||
from game.game import Game
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
||||
from theater.missiontarget import MissionTarget
|
||||
|
||||
|
||||
class DeletableChildModelManager:
|
||||
"""Manages lifetimes for child models.
|
||||
|
||||
Qt's data models don't have a good way of modeling related data aside from
|
||||
lists, tables, or trees of similar objects. We could build one monolithic
|
||||
GameModel that tracks all of the data in the game and use the parent/child
|
||||
relationships of that model to index down into the ATO, packages, flights,
|
||||
etc, but doing so is error prone because it requires us to manually manage
|
||||
that relationship tree and keep our own mappings from row/column into
|
||||
specific members.
|
||||
|
||||
However, creating child models outside of the tree means that removing an
|
||||
item from the parent will not signal the child's deletion to any views, so
|
||||
we must track this explicitly.
|
||||
|
||||
Any model which has child data types should use this class to track the
|
||||
deletion of child models. All child model types must define a signal named
|
||||
`deleted`. This signal will be emitted when the child model is being
|
||||
deleted. Any views displaying such data should subscribe to those events and
|
||||
update their display accordingly.
|
||||
"""
|
||||
|
||||
#: The type of data owned by models created by this class.
|
||||
DataType = TypeVar("DataType")
|
||||
|
||||
#: The type of model managed by this class.
|
||||
ModelType = TypeVar("ModelType")
|
||||
|
||||
ModelDict = Dict[DataType, ModelType]
|
||||
|
||||
def __init__(self, create_model: Callable[[DataType], ModelType]) -> None:
|
||||
self.create_model = create_model
|
||||
self.models: DeletableChildModelManager.ModelDict = {}
|
||||
|
||||
def acquire(self, data: DataType) -> ModelType:
|
||||
"""Returns a model for the given child data.
|
||||
|
||||
If a model has already been created for the given data, it will be
|
||||
returned. The data type must be hashable.
|
||||
"""
|
||||
if data in self.models:
|
||||
return self.models[data]
|
||||
model = self.create_model(data)
|
||||
self.models[data] = model
|
||||
return model
|
||||
|
||||
def release(self, data: DataType) -> None:
|
||||
"""Releases the model matching the given data, if one exists.
|
||||
|
||||
If the given data has had a model created for it, that model will be
|
||||
deleted and its `deleted` signal will be emitted.
|
||||
"""
|
||||
if data in self.models:
|
||||
model = self.models[data]
|
||||
del self.models[data]
|
||||
model.deleted.emit()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Deletes all managed models."""
|
||||
for data in list(self.models.keys()):
|
||||
self.release(data)
|
||||
|
||||
|
||||
class NullListModel(QAbstractListModel):
|
||||
"""Generic empty list model."""
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return 0
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
class PackageModel(QAbstractListModel):
|
||||
"""The model for an ATO package."""
|
||||
|
||||
#: Emitted when this package is being deleted from the ATO.
|
||||
deleted = Signal()
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
super().__init__()
|
||||
self.package = package
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return len(self.package.flights)
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
if not index.isValid():
|
||||
return None
|
||||
flight = self.flight_at_index(index)
|
||||
if role == Qt.DisplayRole:
|
||||
return self.text_for_flight(flight)
|
||||
if role == Qt.DecorationRole:
|
||||
return self.icon_for_flight(flight)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def text_for_flight(flight: Flight) -> str:
|
||||
"""Returns the text that should be displayed for the flight."""
|
||||
task = flight.flight_type.name
|
||||
count = flight.count
|
||||
name = db.unit_type_name(flight.unit_type)
|
||||
delay = flight.scheduled_in
|
||||
origin = flight.from_cp.name
|
||||
return f"[{task}] {count} x {name} from {origin} in {delay} minutes"
|
||||
|
||||
@staticmethod
|
||||
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
|
||||
"""Returns the icon that should be displayed for the flight."""
|
||||
name = db.unit_type_name(flight.unit_type)
|
||||
if name in AIRCRAFT_ICONS:
|
||||
return QIcon(AIRCRAFT_ICONS[name])
|
||||
return None
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds the given flight to the package."""
|
||||
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
||||
self.package.add_flight(flight)
|
||||
self.endInsertRows()
|
||||
|
||||
def delete_flight_at_index(self, index: QModelIndex) -> None:
|
||||
"""Removes the flight at the given index from the package."""
|
||||
self.delete_flight(self.flight_at_index(index))
|
||||
|
||||
def delete_flight(self, flight: Flight) -> None:
|
||||
"""Removes the given flight from the package.
|
||||
|
||||
If the flight is using claimed inventory, the caller is responsible for
|
||||
returning that inventory.
|
||||
"""
|
||||
index = self.package.flights.index(flight)
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
self.package.remove_flight(flight)
|
||||
self.endRemoveRows()
|
||||
|
||||
def flight_at_index(self, index: QModelIndex) -> Flight:
|
||||
"""Returns the flight located at the given index."""
|
||||
return self.package.flights[index.row()]
|
||||
|
||||
@property
|
||||
def mission_target(self) -> MissionTarget:
|
||||
"""Returns the mission target of the package."""
|
||||
package = self.package
|
||||
target = package.target
|
||||
return target
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""Returns the description of the package."""
|
||||
return self.package.package_description
|
||||
|
||||
@property
|
||||
def flights(self) -> Iterator[Flight]:
|
||||
"""Iterates over the flights in the package."""
|
||||
for flight in self.package.flights:
|
||||
yield flight
|
||||
|
||||
|
||||
class AtoModel(QAbstractListModel):
|
||||
"""The model for an AirTaskingOrder."""
|
||||
|
||||
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
|
||||
super().__init__()
|
||||
self.game = game
|
||||
self.ato = ato
|
||||
self.package_models = DeletableChildModelManager(PackageModel)
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return len(self.ato.packages)
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
if not index.isValid():
|
||||
return None
|
||||
if role == Qt.DisplayRole:
|
||||
package = self.ato.packages[index.row()]
|
||||
return f"{package.package_description} {package.target.name}"
|
||||
return None
|
||||
|
||||
def add_package(self, package: Package) -> None:
|
||||
"""Adds a package to the ATO."""
|
||||
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
||||
self.ato.add_package(package)
|
||||
self.endInsertRows()
|
||||
|
||||
def delete_package_at_index(self, index: QModelIndex) -> None:
|
||||
"""Removes the package at the given index from the ATO."""
|
||||
self.delete_package(self.package_at_index(index))
|
||||
|
||||
def delete_package(self, package: Package) -> None:
|
||||
"""Removes the given package from the ATO."""
|
||||
self.package_models.release(package)
|
||||
index = self.ato.packages.index(package)
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
self.ato.remove_package(package)
|
||||
for flight in package.flights:
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
self.endRemoveRows()
|
||||
|
||||
def package_at_index(self, index: QModelIndex) -> Package:
|
||||
"""Returns the package at the given index."""
|
||||
return self.ato.packages[index.row()]
|
||||
|
||||
def replace_from_game(self, game: Optional[Game]) -> None:
|
||||
"""Updates the ATO object to match the updated game object.
|
||||
|
||||
If the game is None (as is the case when no game has been loaded), an
|
||||
empty ATO will be used.
|
||||
"""
|
||||
self.beginResetModel()
|
||||
self.game = game
|
||||
self.package_models.clear()
|
||||
if self.game is not None:
|
||||
self.ato = game.blue_ato
|
||||
else:
|
||||
self.ato = AirTaskingOrder()
|
||||
self.endResetModel()
|
||||
|
||||
def get_package_model(self, index: QModelIndex) -> PackageModel:
|
||||
"""Returns a model for the package at the given index."""
|
||||
return self.package_models.acquire(self.package_at_index(index))
|
||||
|
||||
@property
|
||||
def packages(self) -> Iterator[PackageModel]:
|
||||
"""Iterates over all the packages in the ATO."""
|
||||
for package in self.ato.packages:
|
||||
yield self.package_models.acquire(package)
|
||||
|
||||
|
||||
class GameModel:
|
||||
"""A model for the Game object.
|
||||
|
||||
This isn't a real Qt data model, but simplifies management of the game and
|
||||
its ATO objects.
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
self.game: Optional[Game] = None
|
||||
# TODO: Add red ATO model, add cheat option to show red flight plan.
|
||||
self.ato_model = AtoModel(self.game, AirTaskingOrder())
|
||||
|
||||
def set(self, game: Optional[Game]) -> None:
|
||||
"""Updates the managed Game object.
|
||||
|
||||
The argument will be None when no game has been loaded. In this state,
|
||||
much of the UI is still visible and needs to handle that behavior. To
|
||||
simplify that case, the AtoModel will model an empty ATO when no game is
|
||||
loaded.
|
||||
"""
|
||||
self.game = game
|
||||
self.ato_model.replace_from_game(self.game)
|
||||
13
qt_ui/widgets/QFlightSizeSpinner.py
Normal file
13
qt_ui/widgets/QFlightSizeSpinner.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Spin box for selecting the number of aircraft in a flight."""
|
||||
from PySide2.QtWidgets import QSpinBox
|
||||
|
||||
|
||||
class QFlightSizeSpinner(QSpinBox):
|
||||
"""Spin box for selecting the number of aircraft in a flight."""
|
||||
|
||||
def __init__(self, min_size: int = 1, max_size: int = 4,
|
||||
default_size: int = 2) -> None:
|
||||
super().__init__()
|
||||
self.setMinimum(min_size)
|
||||
self.setMaximum(max_size)
|
||||
self.setValue(default_size)
|
||||
17
qt_ui/widgets/QLabeledWidget.py
Normal file
17
qt_ui/widgets/QLabeledWidget.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""A layout containing a widget with an associated label."""
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget
|
||||
|
||||
|
||||
class QLabeledWidget(QHBoxLayout):
|
||||
"""A layout containing a widget with an associated label.
|
||||
|
||||
Best used for vertical forms, where the given widget is the input and the
|
||||
label is used to name the input.
|
||||
"""
|
||||
|
||||
def __init__(self, text: str, widget: QWidget) -> None:
|
||||
super().__init__()
|
||||
self.addWidget(QLabel(text))
|
||||
self.addStretch()
|
||||
self.addWidget(widget, alignment=Qt.AlignRight)
|
||||
@ -1,16 +1,15 @@
|
||||
from PySide2.QtWidgets import QFrame, QHBoxLayout, QPushButton, QVBoxLayout, QGroupBox
|
||||
|
||||
from game import Game
|
||||
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
||||
from qt_ui.windows.finances.QFinancesMenu import QFinancesMenu
|
||||
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
|
||||
from qt_ui.widgets.QTurnCounter import QTurnCounter
|
||||
from PySide2.QtWidgets import QFrame, QGroupBox, QHBoxLayout, QPushButton
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game
|
||||
from game.event import CAP, CAS, FrontlineAttackEvent
|
||||
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
||||
from qt_ui.widgets.QTurnCounter import QTurnCounter
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.mission.QMissionPlanning import QMissionPlanning
|
||||
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
|
||||
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
|
||||
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
|
||||
|
||||
|
||||
class QTopPanel(QFrame):
|
||||
@ -33,10 +32,10 @@ class QTopPanel(QFrame):
|
||||
self.passTurnButton.setProperty("style", "btn-primary")
|
||||
self.passTurnButton.clicked.connect(self.passTurn)
|
||||
|
||||
self.proceedButton = QPushButton("Mission Planning")
|
||||
self.proceedButton = QPushButton("Take off")
|
||||
self.proceedButton.setIcon(CONST.ICONS["Proceed"])
|
||||
self.proceedButton.setProperty("style", "btn-success")
|
||||
self.proceedButton.clicked.connect(self.proceed)
|
||||
self.proceedButton.setProperty("style", "start-button")
|
||||
self.proceedButton.clicked.connect(self.launch_mission)
|
||||
if self.game and self.game.turn == 0:
|
||||
self.proceedButton.setEnabled(False)
|
||||
|
||||
@ -100,9 +99,31 @@ class QTopPanel(QFrame):
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
self.proceedButton.setEnabled(True)
|
||||
|
||||
def proceed(self):
|
||||
self.subwindow = QMissionPlanning(self.game)
|
||||
self.subwindow.show()
|
||||
def launch_mission(self):
|
||||
"""Finishes planning and waits for mission completion."""
|
||||
# TODO: Refactor this nonsense.
|
||||
game_event = None
|
||||
for event in self.game.events:
|
||||
if isinstance(event,
|
||||
FrontlineAttackEvent) and event.is_player_attacking:
|
||||
game_event = event
|
||||
if game_event is None:
|
||||
game_event = FrontlineAttackEvent(
|
||||
self.game,
|
||||
self.game.theater.controlpoints[0],
|
||||
self.game.theater.controlpoints[0],
|
||||
self.game.theater.controlpoints[0].position,
|
||||
self.game.player_name,
|
||||
self.game.enemy_name)
|
||||
game_event.is_awacs_enabled = True
|
||||
game_event.ca_slots = 1
|
||||
game_event.departure_cp = self.game.theater.controlpoints[0]
|
||||
game_event.player_attacking({CAS: {}, CAP: {}})
|
||||
game_event.depart_from = self.game.theater.controlpoints[0]
|
||||
|
||||
self.game.initiate_event(game_event)
|
||||
waiting = QWaitingForMissionResultWindow(game_event, self.game)
|
||||
waiting.show()
|
||||
|
||||
def budget_update(self, game:Game):
|
||||
self.budgetBox.setGame(game)
|
||||
|
||||
249
qt_ui/widgets/ato.py
Normal file
249
qt_ui/widgets/ato.py
Normal file
@ -0,0 +1,249 @@
|
||||
"""Widgets for displaying air tasking orders."""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize, Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QListView,
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
||||
|
||||
|
||||
class QFlightList(QListView):
|
||||
"""List view for displaying the flights of a package."""
|
||||
|
||||
def __init__(self, model: Optional[PackageModel]) -> None:
|
||||
super().__init__()
|
||||
self.package_model = model
|
||||
self.set_package(model)
|
||||
self.setIconSize(QSize(91, 24))
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
|
||||
def set_package(self, model: Optional[PackageModel]) -> None:
|
||||
"""Sets the package model to display."""
|
||||
if model is None:
|
||||
self.disconnect_model()
|
||||
else:
|
||||
self.package_model = model
|
||||
self.setModel(model)
|
||||
# noinspection PyUnresolvedReferences
|
||||
model.deleted.connect(self.disconnect_model)
|
||||
self.selectionModel().setCurrentIndex(
|
||||
model.index(0, 0, QModelIndex()),
|
||||
QItemSelectionModel.Select
|
||||
)
|
||||
|
||||
def disconnect_model(self) -> None:
|
||||
"""Clears the listview of any model attachments.
|
||||
|
||||
Displays an empty list until set_package is called with a valid model.
|
||||
"""
|
||||
model = self.model()
|
||||
if model is not None and isinstance(model, PackageModel):
|
||||
model.deleted.disconnect(self.disconnect_model)
|
||||
self.setModel(NullListModel())
|
||||
|
||||
@property
|
||||
def selected_item(self) -> Optional[Flight]:
|
||||
"""Returns the selected flight, if any."""
|
||||
index = self.currentIndex()
|
||||
if not index.isValid():
|
||||
return None
|
||||
return self.package_model.flight_at_index(index)
|
||||
|
||||
|
||||
class QFlightPanel(QGroupBox):
|
||||
"""The flight display portion of the ATO panel.
|
||||
|
||||
Displays the flights assigned to the selected package, and includes edit and
|
||||
delete buttons for flight management.
|
||||
"""
|
||||
|
||||
def __init__(self, game_model: GameModel,
|
||||
package_model: Optional[PackageModel] = None) -> None:
|
||||
super().__init__("Flights")
|
||||
self.game_model = game_model
|
||||
self.package_model = package_model
|
||||
|
||||
self.vbox = QVBoxLayout()
|
||||
self.setLayout(self.vbox)
|
||||
|
||||
self.flight_list = QFlightList(package_model)
|
||||
self.vbox.addWidget(self.flight_list)
|
||||
|
||||
self.button_row = QHBoxLayout()
|
||||
self.vbox.addLayout(self.button_row)
|
||||
|
||||
self.edit_button = QPushButton("Edit")
|
||||
self.edit_button.clicked.connect(self.on_edit)
|
||||
self.button_row.addWidget(self.edit_button)
|
||||
|
||||
self.delete_button = QPushButton("Delete")
|
||||
# noinspection PyTypeChecker
|
||||
self.delete_button.setProperty("style", "btn-danger")
|
||||
self.delete_button.clicked.connect(self.on_delete)
|
||||
self.button_row.addWidget(self.delete_button)
|
||||
|
||||
self.selection_changed.connect(self.on_selection_changed)
|
||||
self.on_selection_changed()
|
||||
|
||||
def set_package(self, model: Optional[PackageModel]) -> None:
|
||||
"""Sets the package model to display."""
|
||||
self.package_model = model
|
||||
self.flight_list.set_package(model)
|
||||
self.on_selection_changed()
|
||||
|
||||
@property
|
||||
def selection_changed(self):
|
||||
"""Returns the signal emitted when the flight selection changes."""
|
||||
return self.flight_list.selectionModel().selectionChanged
|
||||
|
||||
def on_selection_changed(self) -> None:
|
||||
"""Updates the status of the edit and delete buttons."""
|
||||
index = self.flight_list.currentIndex()
|
||||
enabled = index.isValid()
|
||||
self.edit_button.setEnabled(enabled)
|
||||
self.delete_button.setEnabled(enabled)
|
||||
|
||||
def on_edit(self) -> None:
|
||||
"""Opens the flight edit dialog."""
|
||||
index = self.flight_list.currentIndex()
|
||||
if not index.isValid():
|
||||
logging.error(f"Cannot edit flight when no flight is selected.")
|
||||
return
|
||||
from qt_ui.dialogs import Dialog
|
||||
Dialog.open_edit_flight_dialog(
|
||||
self.package_model.flight_at_index(index)
|
||||
)
|
||||
|
||||
def on_delete(self) -> None:
|
||||
"""Removes the selected flight from the package."""
|
||||
index = self.flight_list.currentIndex()
|
||||
if not index.isValid():
|
||||
logging.error(f"Cannot delete flight when no flight is selected.")
|
||||
return
|
||||
self.game_model.game.aircraft_inventory.return_from_flight(
|
||||
self.flight_list.selected_item)
|
||||
self.package_model.delete_flight_at_index(index)
|
||||
|
||||
|
||||
class QPackageList(QListView):
|
||||
"""List view for displaying the packages of an ATO."""
|
||||
|
||||
def __init__(self, model: AtoModel) -> None:
|
||||
super().__init__()
|
||||
self.ato_model = model
|
||||
self.setModel(model)
|
||||
self.setIconSize(QSize(91, 24))
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
|
||||
@property
|
||||
def selected_item(self) -> Optional[Package]:
|
||||
"""Returns the selected package, if any."""
|
||||
index = self.currentIndex()
|
||||
if not index.isValid():
|
||||
return None
|
||||
return self.ato_model.package_at_index(index)
|
||||
|
||||
|
||||
class QPackagePanel(QGroupBox):
|
||||
"""The package display portion of the ATO panel.
|
||||
|
||||
Displays the package assigned to the player's ATO, and includes edit and
|
||||
delete buttons for package management.
|
||||
"""
|
||||
|
||||
def __init__(self, model: AtoModel) -> None:
|
||||
super().__init__("Packages")
|
||||
self.ato_model = model
|
||||
self.ato_model.layoutChanged.connect(self.on_selection_changed)
|
||||
|
||||
self.vbox = QVBoxLayout()
|
||||
self.setLayout(self.vbox)
|
||||
|
||||
self.package_list = QPackageList(self.ato_model)
|
||||
self.vbox.addWidget(self.package_list)
|
||||
|
||||
self.button_row = QHBoxLayout()
|
||||
self.vbox.addLayout(self.button_row)
|
||||
|
||||
self.edit_button = QPushButton("Edit")
|
||||
self.edit_button.clicked.connect(self.on_edit)
|
||||
self.button_row.addWidget(self.edit_button)
|
||||
|
||||
self.delete_button = QPushButton("Delete")
|
||||
# noinspection PyTypeChecker
|
||||
self.delete_button.setProperty("style", "btn-danger")
|
||||
self.delete_button.clicked.connect(self.on_delete)
|
||||
self.button_row.addWidget(self.delete_button)
|
||||
|
||||
self.selection_changed.connect(self.on_selection_changed)
|
||||
self.on_selection_changed()
|
||||
|
||||
@property
|
||||
def selection_changed(self):
|
||||
"""Returns the signal emitted when the flight selection changes."""
|
||||
return self.package_list.selectionModel().selectionChanged
|
||||
|
||||
def on_selection_changed(self) -> None:
|
||||
"""Updates the status of the edit and delete buttons."""
|
||||
index = self.package_list.currentIndex()
|
||||
enabled = index.isValid()
|
||||
self.edit_button.setEnabled(enabled)
|
||||
self.delete_button.setEnabled(enabled)
|
||||
|
||||
def on_edit(self) -> None:
|
||||
"""Opens the package edit dialog."""
|
||||
index = self.package_list.currentIndex()
|
||||
if not index.isValid():
|
||||
logging.error(f"Cannot edit package when no package is selected.")
|
||||
return
|
||||
from qt_ui.dialogs import Dialog
|
||||
Dialog.open_edit_package_dialog(self.ato_model.get_package_model(index))
|
||||
|
||||
def on_delete(self) -> None:
|
||||
"""Removes the package from the ATO."""
|
||||
index = self.package_list.currentIndex()
|
||||
if not index.isValid():
|
||||
logging.error(f"Cannot delete package when no package is selected.")
|
||||
return
|
||||
self.ato_model.delete_package_at_index(index)
|
||||
|
||||
|
||||
class QAirTaskingOrderPanel(QSplitter):
|
||||
"""A split panel for displaying the packages and flights of an ATO.
|
||||
|
||||
Used as the left-bar of the main UI. The top half of the panel displays the
|
||||
packages of the player's ATO, and the bottom half displays the flights of
|
||||
the selected package.
|
||||
"""
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__(Qt.Vertical)
|
||||
self.ato_model = game_model.ato_model
|
||||
|
||||
self.package_panel = QPackagePanel(self.ato_model)
|
||||
self.package_panel.selection_changed.connect(self.on_package_change)
|
||||
self.ato_model.rowsInserted.connect(self.on_package_change)
|
||||
self.addWidget(self.package_panel)
|
||||
|
||||
self.flight_panel = QFlightPanel(game_model)
|
||||
self.addWidget(self.flight_panel)
|
||||
|
||||
def on_package_change(self) -> None:
|
||||
"""Sets the newly selected flight for display in the bottom panel."""
|
||||
index = self.package_panel.package_list.currentIndex()
|
||||
if index.isValid():
|
||||
self.flight_panel.set_package(
|
||||
self.ato_model.get_package_model(index)
|
||||
)
|
||||
else:
|
||||
self.flight_panel.set_package(None)
|
||||
16
qt_ui/widgets/combos/QAircraftTypeSelector.py
Normal file
16
qt_ui/widgets/combos/QAircraftTypeSelector.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""Combo box for selecting aircraft types."""
|
||||
from typing import Iterable
|
||||
|
||||
from PySide2.QtWidgets import QComboBox
|
||||
|
||||
from dcs.planes import PlaneType
|
||||
|
||||
|
||||
class QAircraftTypeSelector(QComboBox):
|
||||
"""Combo box for selecting among the given aircraft types."""
|
||||
|
||||
def __init__(self, aircraft_types: Iterable[PlaneType]) -> None:
|
||||
super().__init__()
|
||||
for aircraft in aircraft_types:
|
||||
self.addItem(f"{aircraft.id}", userData=aircraft)
|
||||
self.model().sort(0)
|
||||
105
qt_ui/widgets/combos/QFlightTypeComboBox.py
Normal file
105
qt_ui/widgets/combos/QFlightTypeComboBox.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""Combo box for selecting a flight's task type."""
|
||||
import logging
|
||||
from typing import Iterator
|
||||
|
||||
from PySide2.QtWidgets import QComboBox
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
from theater import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
FrontLine,
|
||||
MissionTarget,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
|
||||
|
||||
class QFlightTypeComboBox(QComboBox):
|
||||
"""Combo box for selecting a flight task type."""
|
||||
|
||||
COMMON_ENEMY_MISSIONS = [
|
||||
FlightType.TARCAP,
|
||||
FlightType.SEAD,
|
||||
FlightType.DEAD,
|
||||
# TODO: FlightType.ELINT,
|
||||
# TODO: FlightType.ESCORT,
|
||||
# TODO: FlightType.EWAR,
|
||||
# TODO: FlightType.RECON,
|
||||
]
|
||||
|
||||
FRIENDLY_AIRBASE_MISSIONS = [
|
||||
FlightType.CAP,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
|
||||
FRIENDLY_CARRIER_MISSIONS = [
|
||||
FlightType.BARCAP,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: Buddy tanking for the A-4?
|
||||
# TODO: Rescue chopper?
|
||||
# TODO: Inter-ship logistics?
|
||||
]
|
||||
|
||||
ENEMY_CARRIER_MISSIONS = [
|
||||
FlightType.TARCAP,
|
||||
# TODO: FlightType.ANTISHIP
|
||||
# TODO: FlightType.ESCORT,
|
||||
]
|
||||
|
||||
ENEMY_AIRBASE_MISSIONS = [
|
||||
# TODO: FlightType.STRIKE
|
||||
] + COMMON_ENEMY_MISSIONS
|
||||
|
||||
FRIENDLY_GROUND_OBJECT_MISSIONS = [
|
||||
FlightType.CAP,
|
||||
# TODO: FlightType.LOGISTICS
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
]
|
||||
|
||||
ENEMY_GROUND_OBJECT_MISSIONS = [
|
||||
FlightType.STRIKE,
|
||||
] + COMMON_ENEMY_MISSIONS
|
||||
|
||||
FRONT_LINE_MISSIONS = [
|
||||
FlightType.CAS,
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
# TODO: FlightType.EVAC
|
||||
] + COMMON_ENEMY_MISSIONS
|
||||
|
||||
# TODO: Add BAI missions after we have useful BAI targets.
|
||||
|
||||
def __init__(self, theater: ConflictTheater, target: MissionTarget) -> None:
|
||||
super().__init__()
|
||||
self.theater = theater
|
||||
self.target = target
|
||||
for mission_type in self.mission_types_for_target():
|
||||
self.addItem(mission_type.name, userData=mission_type)
|
||||
|
||||
def mission_types_for_target(self) -> Iterator[FlightType]:
|
||||
if isinstance(self.target, ControlPoint):
|
||||
friendly = self.target.captured
|
||||
fleet = self.target.is_fleet
|
||||
if friendly:
|
||||
if fleet:
|
||||
yield from self.FRIENDLY_CARRIER_MISSIONS
|
||||
else:
|
||||
yield from self.FRIENDLY_AIRBASE_MISSIONS
|
||||
else:
|
||||
if fleet:
|
||||
yield from self.ENEMY_CARRIER_MISSIONS
|
||||
else:
|
||||
yield from self.ENEMY_AIRBASE_MISSIONS
|
||||
elif isinstance(self.target, TheaterGroundObject):
|
||||
# TODO: Filter more based on the category.
|
||||
friendly = self.target.parent_control_point(self.theater).captured
|
||||
if friendly:
|
||||
yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS
|
||||
else:
|
||||
yield from self.ENEMY_GROUND_OBJECT_MISSIONS
|
||||
elif isinstance(self.target, FrontLine):
|
||||
yield from self.FRONT_LINE_MISSIONS
|
||||
else:
|
||||
logging.error(
|
||||
f"Unhandled target type: {self.target.__class__.__name__}"
|
||||
)
|
||||
41
qt_ui/widgets/combos/QOriginAirfieldSelector.py
Normal file
41
qt_ui/widgets/combos/QOriginAirfieldSelector.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Combo box for selecting a departure airfield."""
|
||||
from typing import Iterable
|
||||
|
||||
from PySide2.QtWidgets import QComboBox
|
||||
|
||||
from dcs.planes import PlaneType
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from theater.controlpoint import ControlPoint
|
||||
|
||||
|
||||
class QOriginAirfieldSelector(QComboBox):
|
||||
"""A combo box for selecting a flight's departure airfield.
|
||||
|
||||
The combo box will automatically be populated with all departure airfields
|
||||
that have unassigned inventory of the given aircraft type.
|
||||
"""
|
||||
|
||||
def __init__(self, global_inventory: GlobalAircraftInventory,
|
||||
origins: Iterable[ControlPoint],
|
||||
aircraft: PlaneType) -> None:
|
||||
super().__init__()
|
||||
self.global_inventory = global_inventory
|
||||
self.origins = list(origins)
|
||||
self.aircraft = aircraft
|
||||
self.rebuild_selector()
|
||||
|
||||
def change_aircraft(self, aircraft: PlaneType) -> None:
|
||||
if self.aircraft == aircraft:
|
||||
return
|
||||
self.aircraft = aircraft
|
||||
self.rebuild_selector()
|
||||
|
||||
def rebuild_selector(self) -> None:
|
||||
self.clear()
|
||||
for origin in self.origins:
|
||||
inventory = self.global_inventory.for_control_point(origin)
|
||||
available = inventory.available(self.aircraft)
|
||||
if available:
|
||||
self.addItem(f"{origin.name} ({available} available)", origin)
|
||||
self.model().sort(0)
|
||||
self.update()
|
||||
82
qt_ui/widgets/map/QFrontLine.py
Normal file
82
qt_ui/widgets/map/QFrontLine.py
Normal file
@ -0,0 +1,82 @@
|
||||
"""Common base for objects drawn on the game map."""
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QPen
|
||||
from PySide2.QtWidgets import (
|
||||
QAction,
|
||||
QGraphicsLineItem,
|
||||
QGraphicsSceneContextMenuEvent,
|
||||
QGraphicsSceneHoverEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
QMenu,
|
||||
)
|
||||
|
||||
import qt_ui.uiconstants as const
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
|
||||
from theater.missiontarget import MissionTarget
|
||||
|
||||
|
||||
class QFrontLine(QGraphicsLineItem):
|
||||
"""Base class for objects drawn on the game map.
|
||||
|
||||
Game map objects have an on_click behavior that triggers on left click, and
|
||||
change the mouse cursor on hover.
|
||||
"""
|
||||
|
||||
def __init__(self, x1: float, y1: float, x2: float, y2: float,
|
||||
mission_target: MissionTarget) -> None:
|
||||
super().__init__(x1, y1, x2, y2)
|
||||
self.mission_target = mission_target
|
||||
self.new_package_dialog: Optional[QNewPackageDialog] = None
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
pen = QPen(brush=const.COLORS["bright_red"])
|
||||
pen.setColor(const.COLORS["orange"])
|
||||
pen.setWidth(8)
|
||||
self.setPen(pen)
|
||||
|
||||
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.on_click()
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None:
|
||||
menu = QMenu("Menu")
|
||||
|
||||
object_details_action = QAction(self.object_dialog_text)
|
||||
object_details_action.triggered.connect(self.on_click)
|
||||
menu.addAction(object_details_action)
|
||||
|
||||
new_package_action = QAction(f"New package")
|
||||
new_package_action.triggered.connect(self.open_new_package_dialog)
|
||||
menu.addAction(new_package_action)
|
||||
|
||||
menu.exec_(event.screenPos())
|
||||
|
||||
@property
|
||||
def object_dialog_text(self) -> str:
|
||||
"""Text to for the object's dialog in the context menu.
|
||||
|
||||
Right clicking a map object will open a context menu and the first item
|
||||
will open the details dialog for this object. This menu action has the
|
||||
same behavior as the on_click event.
|
||||
|
||||
Return:
|
||||
The text that should be displayed for the menu item.
|
||||
"""
|
||||
return "Details"
|
||||
|
||||
def on_click(self) -> None:
|
||||
"""The action to take when this map object is left-clicked.
|
||||
|
||||
Typically this should open a details view of the object.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def open_new_package_dialog(self) -> None:
|
||||
"""Opens the dialog for planning a new mission package."""
|
||||
Dialog.open_new_package_dialog(self.mission_target)
|
||||
@ -1,26 +1,33 @@
|
||||
import typing
|
||||
from typing import Dict
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from PySide2 import QtCore
|
||||
from PySide2.QtCore import Qt, QRect, QPointF
|
||||
from PySide2.QtGui import QPixmap, QBrush, QColor, QWheelEvent, QPen, QFont
|
||||
from PySide2.QtWidgets import QGraphicsView, QFrame, QGraphicsOpacityEffect
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGraphicsOpacityEffect,
|
||||
QGraphicsScene,
|
||||
QGraphicsView,
|
||||
)
|
||||
from dcs import Point
|
||||
from dcs.mapping import point_from_heading
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game, db
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.event import UnitsDeliveryEvent, Event, ControlPointType
|
||||
from gen import Conflict
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.widgets.map.QLiberationScene import QLiberationScene
|
||||
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
|
||||
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
|
||||
from qt_ui.widgets.map.QFrontLine import QFrontLine
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from theater import ControlPoint
|
||||
from theater import ControlPoint, FrontLine
|
||||
|
||||
|
||||
class QLiberationMap(QGraphicsView):
|
||||
WAYPOINT_SIZE = 4
|
||||
|
||||
instance = None
|
||||
display_rules: Dict[str, bool] = {
|
||||
@ -32,11 +39,10 @@ class QLiberationMap(QGraphicsView):
|
||||
"flight_paths": False
|
||||
}
|
||||
|
||||
def __init__(self, game: Game):
|
||||
def __init__(self, game_model: GameModel):
|
||||
super(QLiberationMap, self).__init__()
|
||||
QLiberationMap.instance = self
|
||||
|
||||
self.frontline_vector_cache = {}
|
||||
self.game_model = game_model
|
||||
|
||||
self.setMinimumSize(800,600)
|
||||
self.setMaximumHeight(2160)
|
||||
@ -45,7 +51,7 @@ class QLiberationMap(QGraphicsView):
|
||||
self.factorized = 1
|
||||
self.init_scene()
|
||||
self.connectSignals()
|
||||
self.setGame(game)
|
||||
self.setGame(game_model.game)
|
||||
|
||||
def init_scene(self):
|
||||
scene = QLiberationScene(self)
|
||||
@ -124,8 +130,10 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
pos = self._transform_point(cp.position)
|
||||
|
||||
scene.addItem(QMapControlPoint(self, pos[0] - CONST.CP_SIZE / 2, pos[1] - CONST.CP_SIZE / 2, CONST.CP_SIZE,
|
||||
CONST.CP_SIZE, cp, self.game))
|
||||
scene.addItem(QMapControlPoint(self, pos[0] - CONST.CP_SIZE / 2,
|
||||
pos[1] - CONST.CP_SIZE / 2,
|
||||
CONST.CP_SIZE,
|
||||
CONST.CP_SIZE, cp, self.game_model))
|
||||
|
||||
if cp.captured:
|
||||
pen = QPen(brush=CONST.COLORS[playerColor])
|
||||
@ -168,38 +176,8 @@ class QLiberationMap(QGraphicsView):
|
||||
if self.get_display_rule("lines"):
|
||||
self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
|
||||
if cp.captured:
|
||||
pen = QPen(brush=CONST.COLORS[playerColor])
|
||||
brush = CONST.COLORS[playerColor+"_transparent"]
|
||||
|
||||
flight_path_pen = QPen(brush=CONST.COLORS[playerColor])
|
||||
flight_path_pen.setColor(CONST.COLORS[playerColor])
|
||||
|
||||
else:
|
||||
pen = QPen(brush=CONST.COLORS[enemyColor])
|
||||
brush = CONST.COLORS[enemyColor+"_transparent"]
|
||||
|
||||
flight_path_pen = QPen(brush=CONST.COLORS[enemyColor])
|
||||
flight_path_pen.setColor(CONST.COLORS[enemyColor])
|
||||
|
||||
flight_path_pen.setWidth(1)
|
||||
flight_path_pen.setStyle(Qt.DashDotLine)
|
||||
|
||||
pos = self._transform_point(cp.position)
|
||||
if self.get_display_rule("flight_paths"):
|
||||
if cp.id in self.game.planners.keys():
|
||||
planner = self.game.planners[cp.id]
|
||||
for flight in planner.flights:
|
||||
scene.addEllipse(pos[0], pos[1], 4, 4)
|
||||
prev_pos = list(pos)
|
||||
for point in flight.points:
|
||||
new_pos = self._transform_point(Point(point.x, point.y))
|
||||
scene.addLine(prev_pos[0]+2, prev_pos[1]+2, new_pos[0]+2, new_pos[1]+2, flight_path_pen)
|
||||
scene.addEllipse(new_pos[0], new_pos[1], 4, 4, pen, brush)
|
||||
prev_pos = list(new_pos)
|
||||
scene.addLine(prev_pos[0] + 2, prev_pos[1] + 2, pos[0] + 2, pos[1] + 2, flight_path_pen)
|
||||
if self.get_display_rule("flight_paths"):
|
||||
self.draw_flight_plans(scene)
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
pos = self._transform_point(cp.position)
|
||||
@ -209,6 +187,40 @@ class QLiberationMap(QGraphicsView):
|
||||
text.setDefaultTextColor(Qt.white)
|
||||
text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1)
|
||||
|
||||
def draw_flight_plans(self, scene) -> None:
|
||||
for package in self.game_model.ato_model.packages:
|
||||
for flight in package.flights:
|
||||
self.draw_flight_plan(scene, flight)
|
||||
|
||||
def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight) -> None:
|
||||
is_player = flight.from_cp.captured
|
||||
pos = self._transform_point(flight.from_cp.position)
|
||||
|
||||
self.draw_waypoint(scene, pos, is_player)
|
||||
prev_pos = tuple(pos)
|
||||
for point in flight.points:
|
||||
new_pos = self._transform_point(Point(point.x, point.y))
|
||||
self.draw_flight_path(scene, prev_pos, new_pos, is_player)
|
||||
self.draw_waypoint(scene, new_pos, is_player)
|
||||
prev_pos = tuple(new_pos)
|
||||
self.draw_flight_path(scene, prev_pos, pos, is_player)
|
||||
|
||||
def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
|
||||
player: bool) -> None:
|
||||
waypoint_pen = self.waypoint_pen(player)
|
||||
waypoint_brush = self.waypoint_brush(player)
|
||||
scene.addEllipse(position[0], position[1], self.WAYPOINT_SIZE,
|
||||
self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush)
|
||||
|
||||
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int],
|
||||
pos1: Tuple[int, int], player: bool):
|
||||
flight_path_pen = self.flight_path_pen(player)
|
||||
# Draw the line to the *middle* of the waypoint.
|
||||
offset = self.WAYPOINT_SIZE // 2
|
||||
scene.addLine(pos0[0] + offset, pos0[1] + offset,
|
||||
pos1[0] + offset, pos1[1] + offset,
|
||||
flight_path_pen)
|
||||
|
||||
def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor):
|
||||
scene = self.scene()
|
||||
pos = self._transform_point(cp.position)
|
||||
@ -234,31 +246,12 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
p1 = point_from_heading(pos2[0], pos2[1], h+180, 25)
|
||||
p2 = point_from_heading(pos2[0], pos2[1], h, 25)
|
||||
frontline_pen = QPen(brush=CONST.COLORS["bright_red"])
|
||||
frontline_pen.setColor(CONST.COLORS["orange"])
|
||||
frontline_pen.setWidth(8)
|
||||
scene.addLine(p1[0], p1[1], p2[0], p2[1], pen=frontline_pen)
|
||||
scene.addItem(QFrontLine(p1[0], p1[1], p2[0], p2[1],
|
||||
FrontLine(cp, connected_cp)))
|
||||
|
||||
else:
|
||||
scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen)
|
||||
|
||||
def _frontline_vector(self, from_cp: ControlPoint, to_cp: ControlPoint):
|
||||
# Cache mechanism to avoid performing frontline vector computation on every frame
|
||||
key = str(from_cp.id) + "_" + str(to_cp.id)
|
||||
if key in self.frontline_vector_cache:
|
||||
return self.frontline_vector_cache[key]
|
||||
else:
|
||||
frontline = Conflict.frontline_vector(from_cp, to_cp, self.game.theater)
|
||||
self.frontline_vector_cache[key] = frontline
|
||||
return frontline
|
||||
|
||||
def _frontline_center(self, from_cp: ControlPoint, to_cp: ControlPoint) -> typing.Optional[Point]:
|
||||
frontline_vector = self._frontline_vector(from_cp, to_cp)
|
||||
if frontline_vector:
|
||||
return frontline_vector[0].point_from_heading(frontline_vector[1], frontline_vector[2]/2)
|
||||
else:
|
||||
return None
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent):
|
||||
|
||||
if event.angleDelta().y() > 0:
|
||||
@ -308,6 +301,29 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
return X > treshold and X or treshold, Y > treshold and Y or treshold
|
||||
|
||||
def base_faction_color_name(self, player: bool) -> str:
|
||||
if player:
|
||||
return self.game.get_player_color()
|
||||
else:
|
||||
return self.game.get_enemy_color()
|
||||
|
||||
def waypoint_pen(self, player: bool) -> QPen:
|
||||
name = self.base_faction_color_name(player)
|
||||
return QPen(brush=CONST.COLORS[name])
|
||||
|
||||
def waypoint_brush(self, player: bool) -> QColor:
|
||||
name = self.base_faction_color_name(player)
|
||||
return CONST.COLORS[f"{name}_transparent"]
|
||||
|
||||
def flight_path_pen(self, player: bool) -> QPen:
|
||||
name = self.base_faction_color_name(player)
|
||||
color = CONST.COLORS[name]
|
||||
pen = QPen(brush=color)
|
||||
pen.setColor(color)
|
||||
pen.setWidth(1)
|
||||
pen.setStyle(Qt.DashDotLine)
|
||||
return pen
|
||||
|
||||
def addBackground(self):
|
||||
scene = self.scene()
|
||||
|
||||
|
||||
@ -1,100 +1,65 @@
|
||||
from PySide2.QtCore import QRect, Qt
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtGui import QColor, QPainter
|
||||
from PySide2.QtWidgets import QGraphicsRectItem, QGraphicsSceneHoverEvent, QGraphicsSceneContextMenuEvent, QMenu, \
|
||||
QAction, QGraphicsSceneMouseEvent
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game
|
||||
import qt_ui.uiconstants as const
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
|
||||
from theater import ControlPoint, db
|
||||
from theater import ControlPoint
|
||||
from .QMapObject import QMapObject
|
||||
|
||||
|
||||
class QMapControlPoint(QGraphicsRectItem):
|
||||
|
||||
def __init__(self, parent, x: float, y: float, w: float, h: float, model: ControlPoint, game: Game):
|
||||
super(QMapControlPoint, self).__init__(x, y, w, h)
|
||||
self.model = model
|
||||
self.game = game
|
||||
class QMapControlPoint(QMapObject):
|
||||
def __init__(self, parent, x: float, y: float, w: float, h: float,
|
||||
control_point: ControlPoint, game_model: GameModel) -> None:
|
||||
super().__init__(x, y, w, h, mission_target=control_point)
|
||||
self.game_model = game_model
|
||||
self.control_point = control_point
|
||||
self.parent = parent
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setZValue(1)
|
||||
self.setToolTip(self.model.name)
|
||||
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
#super(QMapControlPoint, self).paint(painter, option, widget)
|
||||
self.setToolTip(self.control_point.name)
|
||||
self.base_details_dialog: Optional[QBaseMenu2] = None
|
||||
|
||||
def paint(self, painter, option, widget=None) -> None:
|
||||
if self.parent.get_display_rule("cp"):
|
||||
painter.save()
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
painter.setBrush(self.brush_color)
|
||||
painter.setPen(self.pen_color)
|
||||
|
||||
if self.model.has_runway():
|
||||
if self.control_point.has_runway():
|
||||
if self.isUnderMouse():
|
||||
painter.setBrush(CONST.COLORS["white"])
|
||||
painter.setBrush(const.COLORS["white"])
|
||||
painter.setPen(self.pen_color)
|
||||
|
||||
r = option.rect
|
||||
painter.drawEllipse(r.x(), r.y(), r.width(), r.height())
|
||||
|
||||
#gauge = QRect(r.x(),
|
||||
# r.y()+CONST.CP_SIZE/2 + 2,
|
||||
# r.width(),
|
||||
# CONST.CP_SIZE / 4)
|
||||
|
||||
#painter.setBrush(CONST.COLORS["bright_red"])
|
||||
#painter.setPen(CONST.COLORS["black"])
|
||||
#painter.drawRect(gauge)
|
||||
|
||||
#gauge2 = QRect(r.x(),
|
||||
# r.y() + CONST.CP_SIZE / 2 + 2,
|
||||
# r.width()*self.model.base.strength,
|
||||
# CONST.CP_SIZE / 4)
|
||||
|
||||
#painter.setBrush(CONST.COLORS["green"])
|
||||
#painter.drawRect(gauge2)
|
||||
else:
|
||||
# TODO : not drawing sunk carriers. Can be improved to display sunk carrier.
|
||||
pass
|
||||
# TODO: Draw sunk carriers differently.
|
||||
# Either don't draw them at all, or perhaps use a sunk ship icon.
|
||||
painter.restore()
|
||||
|
||||
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
|
||||
self.update()
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
def mouseMoveEvent(self, event:QGraphicsSceneMouseEvent):
|
||||
self.update()
|
||||
|
||||
def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent):
|
||||
self.update()
|
||||
|
||||
def mousePressEvent(self, event:QGraphicsSceneMouseEvent):
|
||||
self.openBaseMenu()
|
||||
#self.contextMenuEvent(event)
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent):
|
||||
|
||||
if self.model.captured:
|
||||
openBaseMenu = QAction("Open base menu")
|
||||
else:
|
||||
openBaseMenu = QAction("Open intel menu")
|
||||
|
||||
openBaseMenu.triggered.connect(self.openBaseMenu)
|
||||
|
||||
menu = QMenu("Menu", self.parent)
|
||||
menu.addAction(openBaseMenu)
|
||||
menu.exec_(event.screenPos())
|
||||
|
||||
|
||||
@property
|
||||
def brush_color(self)->QColor:
|
||||
return self.model.captured and CONST.COLORS["blue"] or CONST.COLORS["super_red"]
|
||||
def brush_color(self) -> QColor:
|
||||
if self.control_point.captured:
|
||||
return const.COLORS["blue"]
|
||||
else:
|
||||
return const.COLORS["super_red"]
|
||||
|
||||
@property
|
||||
def pen_color(self) -> QColor:
|
||||
return self.model.captured and CONST.COLORS["white"] or CONST.COLORS["white"]
|
||||
return const.COLORS["white"]
|
||||
|
||||
def openBaseMenu(self):
|
||||
self.baseMenu = QBaseMenu2(self.window(), self.model, self.game)
|
||||
self.baseMenu.show()
|
||||
@property
|
||||
def object_dialog_text(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return "Open base menu"
|
||||
else:
|
||||
return "Open intel menu"
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.base_details_dialog = QBaseMenu2(
|
||||
self.window(),
|
||||
self.control_point,
|
||||
self.game_model
|
||||
)
|
||||
self.base_details_dialog.show()
|
||||
|
||||
@ -1,86 +1,86 @@
|
||||
from PySide2.QtCore import QPoint, QRect, QPointF, Qt
|
||||
from PySide2.QtGui import QPainter, QBrush
|
||||
from PySide2.QtWidgets import QGraphicsRectItem, QGraphicsItem, QGraphicsSceneHoverEvent, QGraphicsSceneMouseEvent
|
||||
from typing import List, Optional
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import db, Game
|
||||
from PySide2.QtCore import QRect
|
||||
from PySide2.QtGui import QBrush
|
||||
from PySide2.QtWidgets import QGraphicsItem
|
||||
|
||||
import qt_ui.uiconstants as const
|
||||
from game import Game
|
||||
from game.data.building_data import FORTIFICATION_BUILDINGS
|
||||
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
||||
from theater import TheaterGroundObject, ControlPoint
|
||||
from .QMapObject import QMapObject
|
||||
|
||||
|
||||
class QMapGroundObject(QGraphicsRectItem):
|
||||
|
||||
def __init__(self, parent, x: float, y: float, w: float, h: float, cp: ControlPoint, model: TheaterGroundObject, game:Game, buildings=[]):
|
||||
super(QMapGroundObject, self).__init__(x, y, w, h)
|
||||
self.model = model
|
||||
self.cp = cp
|
||||
class QMapGroundObject(QMapObject):
|
||||
def __init__(self, parent, x: float, y: float, w: float, h: float,
|
||||
control_point: ControlPoint,
|
||||
ground_object: TheaterGroundObject, game: Game,
|
||||
buildings: Optional[List[TheaterGroundObject]] = None) -> None:
|
||||
super().__init__(x, y, w, h, mission_target=ground_object)
|
||||
self.ground_object = ground_object
|
||||
self.control_point = control_point
|
||||
self.parent = parent
|
||||
self.game = game
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setZValue(2)
|
||||
self.buildings = buildings
|
||||
self.buildings = buildings if buildings is not None else []
|
||||
self.setFlag(QGraphicsItem.ItemIgnoresTransformations, False)
|
||||
self.ground_object_dialog: Optional[QGroundObjectMenu] = None
|
||||
|
||||
if len(self.model.groups) > 0:
|
||||
if self.ground_object.groups:
|
||||
units = {}
|
||||
for g in self.model.groups:
|
||||
print(g)
|
||||
for g in self.ground_object.groups:
|
||||
for u in g.units:
|
||||
if u.type in units.keys():
|
||||
if u.type in units:
|
||||
units[u.type] = units[u.type]+1
|
||||
else:
|
||||
units[u.type] = 1
|
||||
tooltip = "[" + self.model.obj_name + "]" + "\n"
|
||||
tooltip = "[" + self.ground_object.obj_name + "]" + "\n"
|
||||
for unit in units.keys():
|
||||
tooltip = tooltip + str(unit) + "x" + str(units[unit]) + "\n"
|
||||
self.setToolTip(tooltip[:-1])
|
||||
else:
|
||||
tooltip = "[" + self.model.obj_name + "]" + "\n"
|
||||
tooltip = "[" + self.ground_object.obj_name + "]" + "\n"
|
||||
for building in buildings:
|
||||
if not building.is_dead:
|
||||
tooltip = tooltip + str(building.dcs_identifier) + "\n"
|
||||
self.setToolTip(tooltip[:-1])
|
||||
|
||||
def mousePressEvent(self, event:QGraphicsSceneMouseEvent):
|
||||
self.openEditionMenu()
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
#super(QMapControlPoint, self).paint(painter, option, widget)
|
||||
|
||||
playerIcons = "_blue"
|
||||
enemyIcons = ""
|
||||
def paint(self, painter, option, widget=None) -> None:
|
||||
player_icons = "_blue"
|
||||
enemy_icons = ""
|
||||
|
||||
if self.parent.get_display_rule("go"):
|
||||
painter.save()
|
||||
|
||||
cat = self.model.category
|
||||
if cat == "aa" and self.model.sea_object:
|
||||
cat = self.ground_object.category
|
||||
if cat == "aa" and self.ground_object.sea_object:
|
||||
cat = "ship"
|
||||
|
||||
rect = QRect(option.rect.x()+2,option.rect.y(),option.rect.width()-2,option.rect.height())
|
||||
rect = QRect(option.rect.x() + 2, option.rect.y(),
|
||||
option.rect.width() - 2, option.rect.height())
|
||||
|
||||
is_dead = self.model.is_dead
|
||||
is_dead = self.ground_object.is_dead
|
||||
for building in self.buildings:
|
||||
if not building.is_dead:
|
||||
is_dead = False
|
||||
break
|
||||
|
||||
if not is_dead and not self.cp.captured:
|
||||
painter.drawPixmap(rect, CONST.ICONS[cat + enemyIcons])
|
||||
if not is_dead and not self.control_point.captured:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + enemy_icons])
|
||||
elif not is_dead:
|
||||
painter.drawPixmap(rect, CONST.ICONS[cat + playerIcons])
|
||||
painter.drawPixmap(rect, const.ICONS[cat + player_icons])
|
||||
else:
|
||||
painter.drawPixmap(rect, CONST.ICONS["destroyed"])
|
||||
painter.drawPixmap(rect, const.ICONS["destroyed"])
|
||||
|
||||
self.drawHealthGauge(painter, option)
|
||||
self.draw_health_gauge(painter, option)
|
||||
painter.restore()
|
||||
|
||||
def drawHealthGauge(self, painter, option):
|
||||
def draw_health_gauge(self, painter, option) -> None:
|
||||
units_alive = 0
|
||||
units_dead = 0
|
||||
|
||||
if len(self.model.groups) == 0:
|
||||
if len(self.ground_object.groups) == 0:
|
||||
for building in self.buildings:
|
||||
if building.dcs_identifier in FORTIFICATION_BUILDINGS:
|
||||
continue
|
||||
@ -89,7 +89,7 @@ class QMapGroundObject(QGraphicsRectItem):
|
||||
else:
|
||||
units_alive += 1
|
||||
|
||||
for g in self.model.groups:
|
||||
for g in self.ground_object.groups:
|
||||
units_alive += len(g.units)
|
||||
if hasattr(g, "units_losts"):
|
||||
units_dead += len(g.units_losts)
|
||||
@ -97,22 +97,18 @@ class QMapGroundObject(QGraphicsRectItem):
|
||||
if units_dead + units_alive > 0:
|
||||
ratio = float(units_alive)/(float(units_dead) + float(units_alive))
|
||||
bar_height = ratio * option.rect.height()
|
||||
painter.fillRect(option.rect.x(), option.rect.y(), 2, option.rect.height(), QBrush(CONST.COLORS["dark_red"]))
|
||||
painter.fillRect(option.rect.x(), option.rect.y(), 2, bar_height, QBrush(CONST.COLORS["green"]))
|
||||
|
||||
|
||||
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
|
||||
self.update()
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
def mouseMoveEvent(self, event:QGraphicsSceneMouseEvent):
|
||||
self.update()
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent):
|
||||
self.update()
|
||||
|
||||
def openEditionMenu(self):
|
||||
self.editionMenu = QGroundObjectMenu(self.window(), self.model, self.buildings, self.cp, self.game)
|
||||
self.editionMenu.show()
|
||||
painter.fillRect(option.rect.x(), option.rect.y(), 2,
|
||||
option.rect.height(),
|
||||
QBrush(const.COLORS["dark_red"]))
|
||||
painter.fillRect(option.rect.x(), option.rect.y(), 2, bar_height,
|
||||
QBrush(const.COLORS["green"]))
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.ground_object_dialog = QGroundObjectMenu(
|
||||
self.window(),
|
||||
self.ground_object,
|
||||
self.buildings,
|
||||
self.control_point,
|
||||
self.game
|
||||
)
|
||||
self.ground_object_dialog.show()
|
||||
|
||||
75
qt_ui/widgets/map/QMapObject.py
Normal file
75
qt_ui/widgets/map/QMapObject.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""Common base for objects drawn on the game map."""
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QAction,
|
||||
QGraphicsRectItem,
|
||||
QGraphicsSceneContextMenuEvent,
|
||||
QGraphicsSceneHoverEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
QMenu,
|
||||
)
|
||||
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
|
||||
from theater.missiontarget import MissionTarget
|
||||
|
||||
|
||||
class QMapObject(QGraphicsRectItem):
|
||||
"""Base class for objects drawn on the game map.
|
||||
|
||||
Game map objects have an on_click behavior that triggers on left click, and
|
||||
change the mouse cursor on hover.
|
||||
"""
|
||||
|
||||
def __init__(self, x: float, y: float, w: float, h: float,
|
||||
mission_target: MissionTarget) -> None:
|
||||
super().__init__(x, y, w, h)
|
||||
self.mission_target = mission_target
|
||||
self.new_package_dialog: Optional[QNewPackageDialog] = None
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.on_click()
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None:
|
||||
menu = QMenu("Menu", self.parent)
|
||||
|
||||
object_details_action = QAction(self.object_dialog_text)
|
||||
object_details_action.triggered.connect(self.on_click)
|
||||
menu.addAction(object_details_action)
|
||||
|
||||
new_package_action = QAction(f"New package")
|
||||
new_package_action.triggered.connect(self.open_new_package_dialog)
|
||||
menu.addAction(new_package_action)
|
||||
|
||||
menu.exec_(event.screenPos())
|
||||
|
||||
@property
|
||||
def object_dialog_text(self) -> str:
|
||||
"""Text to for the object's dialog in the context menu.
|
||||
|
||||
Right clicking a map object will open a context menu and the first item
|
||||
will open the details dialog for this object. This menu action has the
|
||||
same behavior as the on_click event.
|
||||
|
||||
Return:
|
||||
The text that should be displayed for the menu item.
|
||||
"""
|
||||
return "Details"
|
||||
|
||||
def on_click(self) -> None:
|
||||
"""The action to take when this map object is left-clicked.
|
||||
|
||||
Typically this should open a details view of the object.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def open_new_package_dialog(self) -> None:
|
||||
"""Opens the dialog for planning a new mission package."""
|
||||
Dialog.open_new_package_dialog(self.mission_target)
|
||||
@ -1,22 +1,36 @@
|
||||
import logging
|
||||
import sys
|
||||
import webbrowser
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QIcon
|
||||
from PySide2.QtWidgets import QWidget, QVBoxLayout, QMainWindow, QAction, QMessageBox, QDesktopWidget, \
|
||||
QSplitter, QFileDialog
|
||||
from PySide2.QtWidgets import (
|
||||
QAction,
|
||||
QDesktopWidget,
|
||||
QFileDialog,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QSplitter,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.uiconstants import URLS
|
||||
from qt_ui.widgets.QTopPanel import QTopPanel
|
||||
from qt_ui.widgets.ato import QAirTaskingOrderPanel
|
||||
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal, DebriefingSignal
|
||||
from qt_ui.windows.GameUpdateSignal import DebriefingSignal, GameUpdateSignal
|
||||
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
|
||||
from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard
|
||||
from qt_ui.windows.infos.QInfoPanel import QInfoPanel
|
||||
from qt_ui.windows.preferences.QLiberationPreferencesWindow import QLiberationPreferencesWindow
|
||||
from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard
|
||||
from qt_ui.windows.preferences.QLiberationPreferencesWindow import \
|
||||
QLiberationPreferencesWindow
|
||||
from userdata import persistency
|
||||
|
||||
|
||||
@ -25,6 +39,10 @@ class QLiberationWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super(QLiberationWindow, self).__init__()
|
||||
|
||||
self.game: Optional[Game] = None
|
||||
self.game_model = GameModel()
|
||||
Dialog.set_game(self.game_model)
|
||||
self.ato_panel = None
|
||||
self.info_panel = None
|
||||
self.setGame(persistency.restore_game())
|
||||
|
||||
@ -44,16 +62,19 @@ class QLiberationWindow(QMainWindow):
|
||||
self.setGeometry(0, 0, screen.width(), screen.height())
|
||||
self.setWindowState(Qt.WindowMaximized)
|
||||
|
||||
|
||||
def initUi(self):
|
||||
|
||||
self.liberation_map = QLiberationMap(self.game)
|
||||
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
|
||||
self.liberation_map = QLiberationMap(self.game_model)
|
||||
self.info_panel = QInfoPanel(self.game)
|
||||
|
||||
hbox = QSplitter(Qt.Horizontal)
|
||||
hbox.addWidget(self.info_panel)
|
||||
hbox.addWidget(self.liberation_map)
|
||||
hbox.setSizes([2, 8])
|
||||
vbox = QSplitter(Qt.Vertical)
|
||||
hbox.addWidget(self.ato_panel)
|
||||
hbox.addWidget(vbox)
|
||||
vbox.addWidget(self.liberation_map)
|
||||
vbox.addWidget(self.info_panel)
|
||||
hbox.setSizes([100, 600])
|
||||
vbox.setSizes([600, 100])
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
vbox.setMargin(0)
|
||||
@ -210,10 +231,11 @@ class QLiberationWindow(QMainWindow):
|
||||
def exit(self):
|
||||
sys.exit(0)
|
||||
|
||||
def setGame(self, game: Game):
|
||||
def setGame(self, game: Optional[Game]):
|
||||
self.game = game
|
||||
if self.info_panel:
|
||||
self.info_panel.setGame(game)
|
||||
self.game_model.set(self.game)
|
||||
|
||||
def showAboutDialog(self):
|
||||
text = "<h3>DCS Liberation " + CONST.VERSION_STRING + "</h3>" + \
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QCloseEvent, QPixmap
|
||||
from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget, QDialog, QGridLayout
|
||||
from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget
|
||||
|
||||
from game import Game
|
||||
from game.event import ControlPointType
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs
|
||||
@ -13,19 +13,20 @@ from theater import ControlPoint
|
||||
|
||||
class QBaseMenu2(QDialog):
|
||||
|
||||
def __init__(self, parent, cp: ControlPoint, game: Game):
|
||||
def __init__(self, parent, cp: ControlPoint, game_model: GameModel):
|
||||
super(QBaseMenu2, self).__init__(parent)
|
||||
|
||||
# Attrs
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.game_model = game_model
|
||||
self.is_carrier = self.cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
|
||||
self.objectName = "menuDialogue"
|
||||
|
||||
# Widgets
|
||||
self.qbase_menu_tab = QBaseMenuTabs(cp, game)
|
||||
self.qbase_menu_tab = QBaseMenuTabs(cp, self.game_model)
|
||||
|
||||
try:
|
||||
game = self.game_model.game
|
||||
self.airport = game.theater.terrain.airport_by_id(self.cp.id)
|
||||
except:
|
||||
self.airport = None
|
||||
@ -70,7 +71,9 @@ class QBaseMenu2(QDialog):
|
||||
self.mainLayout.addWidget(header, 0, 0)
|
||||
self.mainLayout.addWidget(self.topLayoutWidget, 1, 0)
|
||||
self.mainLayout.addWidget(self.qbase_menu_tab, 2, 0)
|
||||
totalBudget = QLabel(QRecruitBehaviour.BUDGET_FORMAT.format(self.game.budget))
|
||||
totalBudget = QLabel(
|
||||
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget)
|
||||
)
|
||||
totalBudget.setObjectName("budgetField")
|
||||
totalBudget.setAlignment(Qt.AlignRight | Qt.AlignBottom)
|
||||
totalBudget.setProperty("style", "budget-label")
|
||||
@ -78,7 +81,7 @@ class QBaseMenu2(QDialog):
|
||||
self.setLayout(self.mainLayout)
|
||||
|
||||
def closeEvent(self, closeEvent:QCloseEvent):
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
|
||||
|
||||
def get_base_image(self):
|
||||
if self.cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from PySide2.QtWidgets import QTabWidget, QFrame, QGridLayout, QLabel
|
||||
from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QTabWidget
|
||||
|
||||
from game import Game
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand
|
||||
from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ
|
||||
from qt_ui.windows.basemenu.ground_forces.QGroundForcesHQ import QGroundForcesHQ
|
||||
@ -10,29 +10,29 @@ from theater import ControlPoint
|
||||
|
||||
class QBaseMenuTabs(QTabWidget):
|
||||
|
||||
def __init__(self, cp: ControlPoint, game: Game):
|
||||
def __init__(self, cp: ControlPoint, game_model: GameModel):
|
||||
super(QBaseMenuTabs, self).__init__()
|
||||
self.cp = cp
|
||||
if cp:
|
||||
|
||||
if not cp.captured:
|
||||
self.intel = QIntelInfo(cp, game)
|
||||
self.intel = QIntelInfo(cp, game_model.game)
|
||||
self.addTab(self.intel, "Intel")
|
||||
if not cp.is_carrier:
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game)
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
||||
self.addTab(self.base_defenses_hq, "Base Defenses")
|
||||
else:
|
||||
if cp.has_runway():
|
||||
self.airfield_command = QAirfieldCommand(cp, game)
|
||||
self.airfield_command = QAirfieldCommand(cp, game_model)
|
||||
self.addTab(self.airfield_command, "Airfield Command")
|
||||
|
||||
if not cp.is_carrier:
|
||||
self.ground_forces_hq = QGroundForcesHQ(cp, game)
|
||||
self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
|
||||
self.addTab(self.ground_forces_hq, "Ground Forces HQ")
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game)
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
||||
self.addTab(self.base_defenses_hq, "Base Defenses")
|
||||
else:
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game)
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
||||
self.addTab(self.base_defenses_hq, "Fleet")
|
||||
|
||||
else:
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
from PySide2.QtWidgets import (
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
)
|
||||
import logging
|
||||
|
||||
from PySide2.QtWidgets import QLabel, QPushButton, \
|
||||
QSizePolicy, QSpacerItem, QGroupBox, QHBoxLayout
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from theater import db
|
||||
|
||||
class QRecruitBehaviour:
|
||||
|
||||
|
||||
class QRecruitBehaviour:
|
||||
game = None
|
||||
cp = None
|
||||
deliveryEvent = None
|
||||
@ -17,14 +23,22 @@ class QRecruitBehaviour:
|
||||
recruitable_types = []
|
||||
BUDGET_FORMAT = "Available Budget: <b>${}M</b>"
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.deliveryEvent = None
|
||||
self.bought_amount_labels = {}
|
||||
self.existing_units_labels = {}
|
||||
self.recruitable_types = []
|
||||
self.update_available_budget()
|
||||
|
||||
def add_purchase_row(self, unit_type, layout, row):
|
||||
@property
|
||||
def budget(self) -> int:
|
||||
return self.game_model.game.budget
|
||||
|
||||
@budget.setter
|
||||
def budget(self, value: int) -> None:
|
||||
self.game_model.game.budget = value
|
||||
|
||||
def add_purchase_row(self, unit_type, layout, row):
|
||||
exist = QGroupBox()
|
||||
exist.setProperty("style", "buy-box")
|
||||
exist.setMaximumHeight(36)
|
||||
@ -102,7 +116,8 @@ class QRecruitBehaviour:
|
||||
parent = parent.parent()
|
||||
for child in parent.children():
|
||||
if child.objectName() == "budgetField":
|
||||
child.setText(QRecruitBehaviour.BUDGET_FORMAT.format(self.game.budget))
|
||||
child.setText(
|
||||
QRecruitBehaviour.BUDGET_FORMAT.format(self.budget))
|
||||
|
||||
def buy(self, unit_type):
|
||||
|
||||
@ -113,9 +128,9 @@ class QRecruitBehaviour:
|
||||
return
|
||||
|
||||
price = db.PRICES[unit_type]
|
||||
if self.game.budget >= price:
|
||||
if self.budget >= price:
|
||||
self.deliveryEvent.deliver({unit_type: 1})
|
||||
self.game.budget -= price
|
||||
self.budget -= price
|
||||
else:
|
||||
# TODO : display modal warning
|
||||
logging.info("Not enough money !")
|
||||
@ -125,13 +140,13 @@ class QRecruitBehaviour:
|
||||
def sell(self, unit_type):
|
||||
if self.deliveryEvent.units.get(unit_type, 0) > 0:
|
||||
price = db.PRICES[unit_type]
|
||||
self.game.budget += price
|
||||
self.budget += price
|
||||
self.deliveryEvent.units[unit_type] = self.deliveryEvent.units[unit_type] - 1
|
||||
if self.deliveryEvent.units[unit_type] == 0:
|
||||
del self.deliveryEvent.units[unit_type]
|
||||
elif self.cp.base.total_units_of_type(unit_type) > 0:
|
||||
price = db.PRICES[unit_type]
|
||||
self.game.budget += price
|
||||
self.budget += price
|
||||
self.cp.base.commit_losses({unit_type: 1})
|
||||
|
||||
self._update_count_label(unit_type)
|
||||
|
||||
@ -1,25 +1,35 @@
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QVBoxLayout, QGridLayout, QGroupBox, QScrollArea, QFrame, QWidget, QHBoxLayout, QLabel
|
||||
from typing import Optional
|
||||
|
||||
from game.event import UnitsDeliveryEvent
|
||||
from qt_ui.uiconstants import ICONS
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from game.event.event import UnitsDeliveryEvent
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
|
||||
from theater import ControlPoint, CAP, CAS, db, ControlPointType
|
||||
from game import Game
|
||||
from theater import CAP, CAS, ControlPoint, db
|
||||
|
||||
|
||||
class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
|
||||
def __init__(self, cp: ControlPoint, game: Game):
|
||||
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
|
||||
QFrame.__init__(self)
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.game_model = game_model
|
||||
self.deliveryEvent: Optional[UnitsDeliveryEvent] = None
|
||||
|
||||
for event in self.game.events:
|
||||
self.bought_amount_labels = {}
|
||||
self.existing_units_labels = {}
|
||||
|
||||
for event in self.game_model.game.events:
|
||||
if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp:
|
||||
self.deliveryEvent = event
|
||||
if not self.deliveryEvent:
|
||||
self.deliveryEvent = self.game.units_delivery_event(self.cp)
|
||||
self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp)
|
||||
|
||||
# Determine maximum number of aircrafts that can be bought
|
||||
self.set_maximum_units(self.cp.available_aircraft_slots)
|
||||
@ -36,8 +46,8 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
main_layout = QVBoxLayout()
|
||||
|
||||
units = {
|
||||
CAP: db.find_unittype(CAP, self.game.player_name),
|
||||
CAS: db.find_unittype(CAS, self.game.player_name),
|
||||
CAP: db.find_unittype(CAP, self.game_model.game.player_name),
|
||||
CAS: db.find_unittype(CAS, self.game_model.game.player_name),
|
||||
}
|
||||
|
||||
scroll_content = QWidget()
|
||||
@ -46,7 +56,8 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
|
||||
for task_type in units.keys():
|
||||
units_column = list(set(units[task_type]))
|
||||
if len(units_column) == 0: continue
|
||||
if len(units_column) == 0:
|
||||
continue
|
||||
units_column.sort(key=lambda x: db.PRICES[x])
|
||||
for unit_type in units_column:
|
||||
if self.cp.is_carrier and not unit_type in db.CARRIER_CAPABLE:
|
||||
|
||||
@ -1,27 +1,30 @@
|
||||
from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
|
||||
from game import Game
|
||||
from qt_ui.widgets.base.QAirportInformation import QAirportInformation
|
||||
from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import QAircraftRecruitmentMenu
|
||||
from PySide2.QtWidgets import QFrame, QGridLayout, QGroupBox, QVBoxLayout
|
||||
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import \
|
||||
QAircraftRecruitmentMenu
|
||||
from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView
|
||||
from theater import ControlPoint
|
||||
|
||||
|
||||
class QAirfieldCommand(QFrame):
|
||||
|
||||
def __init__(self, cp:ControlPoint, game:Game):
|
||||
def __init__(self, cp:ControlPoint, game_model: GameModel):
|
||||
super(QAirfieldCommand, self).__init__()
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.game_model = game_model
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
layout = QGridLayout()
|
||||
layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game), 0, 0)
|
||||
layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0)
|
||||
|
||||
try:
|
||||
planned = QGroupBox("Planned Flights")
|
||||
planned_layout = QVBoxLayout()
|
||||
planned_layout.addWidget(QPlannedFlightsView(self.game.planners[self.cp.id]))
|
||||
planned_layout.addWidget(
|
||||
QPlannedFlightsView(self.game_model, self.cp)
|
||||
)
|
||||
planned.setLayout(planned_layout)
|
||||
layout.addWidget(planned, 0, 1)
|
||||
except:
|
||||
|
||||
@ -1,27 +1,33 @@
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QVBoxLayout, QGridLayout, QGroupBox, QFrame, QWidget, QScrollArea
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.event import UnitsDeliveryEvent
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
|
||||
from theater import ControlPoint, PinpointStrike, db
|
||||
|
||||
|
||||
class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
|
||||
def __init__(self, cp:ControlPoint, game:Game):
|
||||
def __init__(self, cp: ControlPoint, game_model: GameModel):
|
||||
QFrame.__init__(self)
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.game_model = game_model
|
||||
|
||||
self.bought_amount_labels = {}
|
||||
self.existing_units_labels = {}
|
||||
|
||||
for event in self.game.events:
|
||||
for event in self.game_model.game.events:
|
||||
if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp:
|
||||
self.deliveryEvent = event
|
||||
if not self.deliveryEvent:
|
||||
self.deliveryEvent = self.game.units_delivery_event(self.cp)
|
||||
self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp)
|
||||
|
||||
self.init_ui()
|
||||
|
||||
@ -29,7 +35,8 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
main_layout = QVBoxLayout()
|
||||
|
||||
units = {
|
||||
PinpointStrike: db.find_unittype(PinpointStrike, self.game.player_name),
|
||||
PinpointStrike: db.find_unittype(PinpointStrike,
|
||||
self.game_model.game.player_name),
|
||||
}
|
||||
|
||||
scroll_content = QWidget()
|
||||
|
||||
@ -1,21 +1,24 @@
|
||||
from PySide2.QtWidgets import QFrame, QGridLayout
|
||||
|
||||
from game import Game
|
||||
from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import QArmorRecruitmentMenu
|
||||
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import QGroundForcesStrategy
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import \
|
||||
QArmorRecruitmentMenu
|
||||
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import \
|
||||
QGroundForcesStrategy
|
||||
from theater import ControlPoint
|
||||
|
||||
|
||||
class QGroundForcesHQ(QFrame):
|
||||
|
||||
def __init__(self, cp:ControlPoint, game:Game):
|
||||
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
|
||||
super(QGroundForcesHQ, self).__init__()
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.game_model = game_model
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
layout = QGridLayout()
|
||||
layout.addWidget(QArmorRecruitmentMenu(self.cp, self.game), 0, 0)
|
||||
layout.addWidget(QGroundForcesStrategy(self.cp, self.game), 0, 1)
|
||||
layout.addWidget(QArmorRecruitmentMenu(self.cp, self.game_model), 0, 0)
|
||||
layout.addWidget(QGroundForcesStrategy(self.cp, self.game_model.game),
|
||||
0, 1)
|
||||
self.setLayout(layout)
|
||||
|
||||
29
qt_ui/windows/mission/QEditFlightDialog.py
Normal file
29
qt_ui/windows/mission/QEditFlightDialog.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""Dialog window for editing flights."""
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
||||
|
||||
|
||||
class QEditFlightDialog(QDialog):
|
||||
"""Dialog window for editing flight plans and loadouts."""
|
||||
|
||||
def __init__(self, game: Game, flight: Flight) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.game = game
|
||||
|
||||
self.setWindowTitle("Create flight")
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
self.flight_planner = QFlightPlanner(flight, game)
|
||||
layout.addWidget(self.flight_planner)
|
||||
|
||||
self.setLayout(layout)
|
||||
@ -1,159 +0,0 @@
|
||||
from PySide2.QtCore import Qt, Slot, QItemSelectionModel, QPoint
|
||||
from PySide2.QtWidgets import QDialog, QGridLayout, QScrollArea, QVBoxLayout, QPushButton, QHBoxLayout, QMessageBox
|
||||
from game import Game
|
||||
from game.event import CAP, CAS, FrontlineAttackEvent
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
|
||||
from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView
|
||||
from qt_ui.windows.mission.QChooseAirbase import QChooseAirbase
|
||||
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
|
||||
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
||||
|
||||
|
||||
class QMissionPlanning(QDialog):
|
||||
|
||||
def __init__(self, game: Game):
|
||||
super(QMissionPlanning, self).__init__()
|
||||
self.game = game
|
||||
self.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
self.setMinimumSize(1000, 440)
|
||||
self.setWindowTitle("Mission Preparation")
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
self.init_ui()
|
||||
print("DONE")
|
||||
|
||||
def init_ui(self):
|
||||
|
||||
self.captured_cp = [cp for cp in self.game.theater.controlpoints if cp.captured]
|
||||
|
||||
self.layout = QGridLayout()
|
||||
self.left_bar_layout = QVBoxLayout()
|
||||
|
||||
self.select_airbase = QChooseAirbase(self.game)
|
||||
self.select_airbase.selected_airbase_changed.connect(self.on_departure_cp_changed)
|
||||
self.planned_flight_view = QPlannedFlightsView(None)
|
||||
self.available_aircraft_at_selected_location = {}
|
||||
if self.captured_cp[0].id in self.game.planners.keys():
|
||||
self.planner = self.game.planners[self.captured_cp[0].id]
|
||||
self.planned_flight_view.set_flight_planner(self.planner)
|
||||
self.selected_cp = self.captured_cp[0]
|
||||
self.available_aircraft_at_selected_location = self.planner.get_available_aircraft()
|
||||
|
||||
self.planned_flight_view.selectionModel().setCurrentIndex(self.planned_flight_view.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows)
|
||||
self.planned_flight_view.selectionModel().selectionChanged.connect(self.on_flight_selection_change)
|
||||
|
||||
if len(self.planned_flight_view.flight_planner.flights) > 0:
|
||||
self.flight_planner = QFlightPlanner(self.planned_flight_view.flight_planner.flights[0], self.game, self.planned_flight_view.flight_planner, 0)
|
||||
self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view)
|
||||
else:
|
||||
self.flight_planner = QFlightPlanner(None, self.game, self.planned_flight_view.flight_planner, 0)
|
||||
self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view)
|
||||
|
||||
self.add_flight_button = QPushButton("Add Flight")
|
||||
self.add_flight_button.clicked.connect(self.on_add_flight)
|
||||
self.delete_flight_button = QPushButton("Delete Selected")
|
||||
self.delete_flight_button.setProperty("style", "btn-danger")
|
||||
self.delete_flight_button.clicked.connect(self.on_delete_flight)
|
||||
|
||||
self.button_layout = QHBoxLayout()
|
||||
self.button_layout.addStretch()
|
||||
self.button_layout.addWidget(self.delete_flight_button)
|
||||
self.button_layout.addWidget(self.add_flight_button)
|
||||
|
||||
self.mission_start_button = QPushButton("Take Off")
|
||||
self.mission_start_button.setProperty("style", "start-button")
|
||||
self.mission_start_button.clicked.connect(self.on_start)
|
||||
|
||||
self.left_bar_layout.addWidget(self.select_airbase)
|
||||
self.left_bar_layout.addWidget(self.planned_flight_view)
|
||||
self.left_bar_layout.addLayout(self.button_layout)
|
||||
|
||||
self.layout.addLayout(self.left_bar_layout, 0, 0)
|
||||
self.layout.addWidget(self.flight_planner, 0, 1)
|
||||
self.layout.addWidget(self.mission_start_button, 1, 1, alignment=Qt.AlignRight)
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
@Slot(str)
|
||||
def on_departure_cp_changed(self, cp_name):
|
||||
cps = [cp for cp in self.game.theater.controlpoints if cp.name == cp_name]
|
||||
|
||||
print(cps)
|
||||
|
||||
if len(cps) == 1:
|
||||
self.selected_cp = cps[0]
|
||||
self.planner = self.game.planners[cps[0].id]
|
||||
self.available_aircraft_at_selected_location = self.planner.get_available_aircraft()
|
||||
self.planned_flight_view.set_flight_planner(self.planner)
|
||||
else:
|
||||
self.available_aircraft_at_selected_location = {}
|
||||
self.planned_flight_view.set_flight_planner(None)
|
||||
|
||||
def on_flight_selection_change(self):
|
||||
|
||||
print("On flight selection change")
|
||||
|
||||
index = self.planned_flight_view.selectionModel().currentIndex().row()
|
||||
self.planned_flight_view.repaint()
|
||||
|
||||
if self.flight_planner is not None:
|
||||
self.flight_planner.on_planned_flight_changed.disconnect()
|
||||
self.flight_planner.clearTabs()
|
||||
|
||||
try:
|
||||
flight = self.planner.flights[index]
|
||||
except IndexError:
|
||||
flight = None
|
||||
self.flight_planner = QFlightPlanner(flight, self.game, self.planner, self.flight_planner.currentIndex())
|
||||
self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view)
|
||||
self.layout.addWidget(self.flight_planner, 0, 1)
|
||||
|
||||
def update_planned_flight_view(self):
|
||||
self.planned_flight_view.update_content()
|
||||
|
||||
def on_add_flight(self):
|
||||
possible_aircraft_type = list(self.selected_cp.base.aircraft.keys())
|
||||
|
||||
if len(possible_aircraft_type) == 0:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Information)
|
||||
msg.setText("No more aircraft are available on " + self.selected_cp.name + " airbase.")
|
||||
msg.setWindowTitle("No more aircraft")
|
||||
msg.setStandardButtons(QMessageBox.Ok)
|
||||
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
msg.exec_()
|
||||
else:
|
||||
self.subwindow = QFlightCreator(self.game, self.selected_cp, possible_aircraft_type, self.planned_flight_view)
|
||||
self.subwindow.show()
|
||||
|
||||
def on_delete_flight(self):
|
||||
index = self.planned_flight_view.selectionModel().currentIndex().row()
|
||||
self.planner.remove_flight(index)
|
||||
self.planned_flight_view.set_flight_planner(self.planner, index)
|
||||
|
||||
|
||||
def on_start(self):
|
||||
|
||||
# TODO : refactor this nonsense
|
||||
self.gameEvent = None
|
||||
for event in self.game.events:
|
||||
if isinstance(event, FrontlineAttackEvent) and event.is_player_attacking:
|
||||
self.gameEvent = event
|
||||
if self.gameEvent is None:
|
||||
self.gameEvent = FrontlineAttackEvent(self.game, self.game.theater.controlpoints[0], self.game.theater.controlpoints[0],
|
||||
self.game.theater.controlpoints[0].position, self.game.player_name, self.game.enemy_name)
|
||||
#if self.awacs_checkbox.isChecked() == 1:
|
||||
# self.gameEvent.is_awacs_enabled = True
|
||||
# self.game.awacs_expense_commit()
|
||||
#else:
|
||||
# self.gameEvent.is_awacs_enabled = False
|
||||
self.gameEvent.is_awacs_enabled = True
|
||||
self.gameEvent.ca_slots = 1
|
||||
self.gameEvent.departure_cp = self.game.theater.controlpoints[0]
|
||||
self.gameEvent.player_attacking({CAS:{}, CAP:{}})
|
||||
self.gameEvent.depart_from = self.game.theater.controlpoints[0]
|
||||
|
||||
self.game.initiate_event(self.gameEvent)
|
||||
waiting = QWaitingForMissionResultWindow(self.gameEvent, self.game)
|
||||
waiting.show()
|
||||
self.close()
|
||||
198
qt_ui/windows/mission/QPackageDialog.py
Normal file
198
qt_ui/windows/mission/QPackageDialog.py
Normal file
@ -0,0 +1,198 @@
|
||||
"""Dialogs for creating and editing ATO packages."""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import QItemSelection, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from game.game import Game
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.models import AtoModel, PackageModel
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.widgets.ato import QFlightList
|
||||
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
|
||||
from theater.missiontarget import MissionTarget
|
||||
|
||||
|
||||
class QPackageDialog(QDialog):
|
||||
"""Base package management dialog.
|
||||
|
||||
The dialogs for creating a new package and editing an existing dialog are
|
||||
very similar, and this implements the shared behavior.
|
||||
"""
|
||||
|
||||
#: Emitted when a change is made to the package.
|
||||
package_changed = Signal()
|
||||
|
||||
#: Emitted when a flight is added to the package.
|
||||
flight_added = Signal(Flight)
|
||||
|
||||
#: Emitted when a flight is removed from the package.
|
||||
flight_removed = Signal(Flight)
|
||||
|
||||
def __init__(self, game: Game, model: PackageModel) -> None:
|
||||
super().__init__()
|
||||
self.game = game
|
||||
self.package_model = model
|
||||
self.add_flight_dialog: Optional[QFlightCreator] = None
|
||||
|
||||
self.setMinimumSize(1000, 440)
|
||||
self.setWindowTitle(
|
||||
f"Mission Package: {self.package_model.mission_target.name}"
|
||||
)
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
|
||||
self.summary_row = QHBoxLayout()
|
||||
self.layout.addLayout(self.summary_row)
|
||||
|
||||
self.package_type_label = QLabel("Package Type:")
|
||||
self.package_type_text = QLabel(self.package_model.description)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_changed.connect(lambda: self.package_type_text.setText(
|
||||
self.package_model.description
|
||||
))
|
||||
self.summary_row.addWidget(self.package_type_label)
|
||||
self.summary_row.addWidget(self.package_type_text)
|
||||
|
||||
self.package_view = QFlightList(self.package_model)
|
||||
self.package_view.selectionModel().selectionChanged.connect(
|
||||
self.on_selection_changed
|
||||
)
|
||||
self.layout.addWidget(self.package_view)
|
||||
|
||||
self.button_layout = QHBoxLayout()
|
||||
self.layout.addLayout(self.button_layout)
|
||||
|
||||
self.add_flight_button = QPushButton("Add Flight")
|
||||
self.add_flight_button.clicked.connect(self.on_add_flight)
|
||||
self.button_layout.addWidget(self.add_flight_button)
|
||||
|
||||
self.delete_flight_button = QPushButton("Delete Selected")
|
||||
self.delete_flight_button.setProperty("style", "btn-danger")
|
||||
self.delete_flight_button.clicked.connect(self.on_delete_flight)
|
||||
self.delete_flight_button.setEnabled(False)
|
||||
self.button_layout.addWidget(self.delete_flight_button)
|
||||
|
||||
self.button_layout.addStretch()
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def on_selection_changed(self, selected: QItemSelection,
|
||||
_deselected: QItemSelection) -> None:
|
||||
"""Updates the state of the delete button."""
|
||||
self.delete_flight_button.setEnabled(not selected.empty())
|
||||
|
||||
def on_add_flight(self) -> None:
|
||||
"""Opens the new flight dialog."""
|
||||
self.add_flight_dialog = QFlightCreator(
|
||||
self.game, self.package_model.package
|
||||
)
|
||||
self.add_flight_dialog.created.connect(self.add_flight)
|
||||
self.add_flight_dialog.show()
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds the new flight to the package."""
|
||||
self.package_model.add_flight(flight)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_changed.emit()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_added.emit(flight)
|
||||
|
||||
def on_delete_flight(self) -> None:
|
||||
"""Removes the selected flight from the package."""
|
||||
flight = self.package_view.selected_item
|
||||
if flight is None:
|
||||
logging.error(f"Cannot delete flight when no flight is selected.")
|
||||
return
|
||||
self.package_model.delete_flight(flight)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_changed.emit()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_removed.emit(flight)
|
||||
|
||||
|
||||
class QNewPackageDialog(QPackageDialog):
|
||||
"""Dialog window for creating a new package.
|
||||
|
||||
New packages do not affect the ATO model until they are saved.
|
||||
"""
|
||||
|
||||
def __init__(self, game: Game, model: AtoModel,
|
||||
target: MissionTarget) -> None:
|
||||
super().__init__(game, PackageModel(Package(target)))
|
||||
self.ato_model = model
|
||||
|
||||
self.save_button = QPushButton("Save")
|
||||
self.save_button.setProperty("style", "start-button")
|
||||
self.save_button.clicked.connect(self.on_save)
|
||||
self.button_layout.addWidget(self.save_button)
|
||||
|
||||
self.delete_flight_button.clicked.connect(self.on_delete_flight)
|
||||
|
||||
def on_save(self) -> None:
|
||||
"""Saves the created package.
|
||||
|
||||
Empty packages may be created. They can be modified later, and will have
|
||||
no effect if empty when the mission is generated.
|
||||
"""
|
||||
self.ato_model.add_package(self.package_model.package)
|
||||
for flight in self.package_model.package.flights:
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
self.close()
|
||||
|
||||
|
||||
class QEditPackageDialog(QPackageDialog):
|
||||
"""Dialog window for editing an existing package.
|
||||
|
||||
Changes to existing packages occur immediately.
|
||||
"""
|
||||
|
||||
def __init__(self, game: Game, model: AtoModel,
|
||||
package: PackageModel) -> None:
|
||||
super().__init__(game, package)
|
||||
self.ato_model = model
|
||||
|
||||
self.delete_button = QPushButton("Delete package")
|
||||
self.delete_button.setProperty("style", "btn-danger")
|
||||
self.delete_button.clicked.connect(self.on_delete)
|
||||
self.button_layout.addWidget(self.delete_button)
|
||||
|
||||
self.done_button = QPushButton("Done")
|
||||
self.done_button.setProperty("style", "start-button")
|
||||
self.done_button.clicked.connect(self.on_done)
|
||||
self.button_layout.addWidget(self.done_button)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_added.connect(self.on_flight_added)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_removed.connect(self.on_flight_removed)
|
||||
|
||||
# TODO: Make the new package dialog do this too, return on cancel.
|
||||
# Not claiming the aircraft when they are added to the planner means that
|
||||
# inventory counts are not updated until after the new package is updated,
|
||||
# so you can add an infinite number of aircraft to a new package in the UI,
|
||||
# which will crash when the flight package is saved.
|
||||
def on_flight_added(self, flight: Flight) -> None:
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
|
||||
def on_flight_removed(self, flight: Flight) -> None:
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
|
||||
def on_done(self) -> None:
|
||||
"""Closes the window."""
|
||||
self.close()
|
||||
|
||||
def on_delete(self) -> None:
|
||||
"""Removes the viewed package from the ATO."""
|
||||
# The ATO model returns inventory for us when deleting a package.
|
||||
self.ato_model.delete_package(self.package_model.package)
|
||||
self.close()
|
||||
@ -1,37 +1,36 @@
|
||||
from PySide2.QtCore import QSize, QItemSelectionModel, QPoint
|
||||
from PySide2.QtCore import QItemSelectionModel, QSize
|
||||
from PySide2.QtGui import QStandardItemModel
|
||||
from PySide2.QtWidgets import QListView, QAbstractItemView
|
||||
from PySide2.QtWidgets import QAbstractItemView, QListView
|
||||
|
||||
from gen.flights.ai_flight_planner import FlightPlanner
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.mission.QFlightItem import QFlightItem
|
||||
from theater.controlpoint import ControlPoint
|
||||
|
||||
|
||||
class QPlannedFlightsView(QListView):
|
||||
|
||||
def __init__(self, flight_planner: FlightPlanner):
|
||||
def __init__(self, game_model: GameModel, cp: ControlPoint) -> None:
|
||||
super(QPlannedFlightsView, self).__init__()
|
||||
self.game_model = game_model
|
||||
self.cp = cp
|
||||
self.model = QStandardItemModel(self)
|
||||
self.setModel(self.model)
|
||||
self.flightitems = []
|
||||
self.flight_items = []
|
||||
self.setIconSize(QSize(91, 24))
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
if flight_planner:
|
||||
self.set_flight_planner(flight_planner)
|
||||
self.set_flight_planner()
|
||||
|
||||
def update_content(self):
|
||||
for i, f in enumerate(self.flight_planner.flights):
|
||||
self.flightitems[i].update(f)
|
||||
def setup_content(self):
|
||||
self.flight_items = []
|
||||
for package in self.game_model.ato_model.packages:
|
||||
for flight in package.flights:
|
||||
if flight.from_cp == self.cp:
|
||||
item = QFlightItem(flight)
|
||||
self.model.appendRow(item)
|
||||
self.flight_items.append(item)
|
||||
self.set_selected_flight(0)
|
||||
|
||||
def setup_content(self, row=0):
|
||||
self.flightitems = []
|
||||
for i, f in enumerate(self.flight_planner.flights):
|
||||
item = QFlightItem(f)
|
||||
self.model.appendRow(item)
|
||||
self.flightitems.append(item)
|
||||
self.setSelectedFlight(row)
|
||||
self.repaint()
|
||||
|
||||
def setSelectedFlight(self, row):
|
||||
def set_selected_flight(self, row):
|
||||
self.selectionModel().clearSelection()
|
||||
index = self.model.index(row, 0)
|
||||
if not index.isValid():
|
||||
@ -42,8 +41,6 @@ class QPlannedFlightsView(QListView):
|
||||
def clear_layout(self):
|
||||
self.model.removeRows(0, self.model.rowCount())
|
||||
|
||||
def set_flight_planner(self, flight_planner: FlightPlanner, row=0):
|
||||
def set_flight_planner(self) -> None:
|
||||
self.clear_layout()
|
||||
self.flight_planner = flight_planner
|
||||
if self.flight_planner:
|
||||
self.setup_content(row)
|
||||
self.setup_content()
|
||||
|
||||
@ -1,122 +1,178 @@
|
||||
from typing import List
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QDialog, QGridLayout, QLabel, QComboBox, QHBoxLayout, QVBoxLayout, QPushButton, QSpinBox, \
|
||||
QMessageBox
|
||||
from dcs import Point
|
||||
from dcs.unittype import UnitType
|
||||
from PySide2.QtCore import Qt, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
from dcs.planes import PlaneType
|
||||
|
||||
from game import Game
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner import FlightPlanner
|
||||
from gen.flights.flight import Flight, FlightWaypoint, FlightType
|
||||
from gen.flights.flight import Flight, FlightType
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox
|
||||
from theater import ControlPoint
|
||||
|
||||
PREDEFINED_WAYPOINT_CATEGORIES = [
|
||||
"Frontline (CAS AREA)",
|
||||
"Building",
|
||||
"Units",
|
||||
"Airbase"
|
||||
]
|
||||
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector
|
||||
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
|
||||
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
|
||||
from theater import ControlPoint, FrontLine, TheaterGroundObject
|
||||
|
||||
|
||||
class QFlightCreator(QDialog):
|
||||
created = Signal(Flight)
|
||||
|
||||
def __init__(self, game: Game, package: Package) -> None:
|
||||
super().__init__()
|
||||
|
||||
def __init__(self, game: Game, from_cp:ControlPoint, possible_aircraft_type:List[UnitType], flight_view=None):
|
||||
super(QFlightCreator, self).__init__()
|
||||
self.game = game
|
||||
self.from_cp = from_cp
|
||||
self.flight_view = flight_view
|
||||
self.planner = self.game.planners[from_cp.id]
|
||||
self.available = self.planner.get_available_aircraft()
|
||||
self.package = package
|
||||
|
||||
self.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
self.setModal(True)
|
||||
self.setWindowTitle("Create flight")
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
|
||||
self.select_type_aircraft = QComboBox()
|
||||
for aircraft_type in self.planner.get_available_aircraft().keys():
|
||||
print(aircraft_type)
|
||||
print(aircraft_type.name)
|
||||
if self.available[aircraft_type] > 0:
|
||||
self.select_type_aircraft.addItem(aircraft_type.id, userData=aircraft_type)
|
||||
self.select_type_aircraft.setCurrentIndex(0)
|
||||
|
||||
self.select_flight_type = QComboBox()
|
||||
self.select_flight_type.addItem("CAP [Combat Air Patrol]", userData=FlightType.CAP)
|
||||
self.select_flight_type.addItem("BARCAP [Barrier Combat Air Patrol]", userData=FlightType.BARCAP)
|
||||
self.select_flight_type.addItem("TARCAP [Target Combat Air Patrol]", userData=FlightType.TARCAP)
|
||||
self.select_flight_type.addItem("INTERCEPT [Interception]", userData=FlightType.INTERCEPTION)
|
||||
self.select_flight_type.addItem("CAS [Close Air Support]", userData=FlightType.CAS)
|
||||
self.select_flight_type.addItem("BAI [Battlefield Interdiction]", userData=FlightType.BAI)
|
||||
self.select_flight_type.addItem("SEAD [Suppression of Enemy Air Defenses]", userData=FlightType.SEAD)
|
||||
self.select_flight_type.addItem("DEAD [Destruction of Enemy Air Defenses]", userData=FlightType.DEAD)
|
||||
self.select_flight_type.addItem("STRIKE [Strike]", userData=FlightType.STRIKE)
|
||||
self.select_flight_type.addItem("ANTISHIP [Antiship Attack]", userData=FlightType.ANTISHIP)
|
||||
self.select_flight_type.setCurrentIndex(0)
|
||||
|
||||
self.select_count_of_aircraft = QSpinBox()
|
||||
self.select_count_of_aircraft.setMinimum(1)
|
||||
self.select_count_of_aircraft.setMaximum(4)
|
||||
self.select_count_of_aircraft.setValue(2)
|
||||
|
||||
aircraft_type = self.select_type_aircraft.currentData()
|
||||
if aircraft_type is not None:
|
||||
self.select_count_of_aircraft.setValue(min(self.available[aircraft_type], 2))
|
||||
self.select_count_of_aircraft.setMaximum(min(self.available[aircraft_type], 4))
|
||||
|
||||
self.add_button = QPushButton("Add")
|
||||
self.add_button.clicked.connect(self.create_flight)
|
||||
|
||||
self.init_ui()
|
||||
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
type_layout = QHBoxLayout()
|
||||
type_layout.addWidget(QLabel("Type of Aircraft : "))
|
||||
type_layout.addStretch()
|
||||
type_layout.addWidget(self.select_type_aircraft, alignment=Qt.AlignRight)
|
||||
self.task_selector = QFlightTypeComboBox(
|
||||
self.game.theater, self.package.target
|
||||
)
|
||||
self.task_selector.setCurrentIndex(0)
|
||||
layout.addLayout(QLabeledWidget("Task:", self.task_selector))
|
||||
|
||||
count_layout = QHBoxLayout()
|
||||
count_layout.addWidget(QLabel("Count : "))
|
||||
count_layout.addStretch()
|
||||
count_layout.addWidget(self.select_count_of_aircraft, alignment=Qt.AlignRight)
|
||||
self.aircraft_selector = QAircraftTypeSelector(
|
||||
self.game.aircraft_inventory.available_types_for_player
|
||||
)
|
||||
self.aircraft_selector.setCurrentIndex(0)
|
||||
self.aircraft_selector.currentIndexChanged.connect(
|
||||
self.on_aircraft_changed)
|
||||
layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector))
|
||||
|
||||
flight_type_layout = QHBoxLayout()
|
||||
flight_type_layout.addWidget(QLabel("Task : "))
|
||||
flight_type_layout.addStretch()
|
||||
flight_type_layout.addWidget(self.select_flight_type, alignment=Qt.AlignRight)
|
||||
self.airfield_selector = QOriginAirfieldSelector(
|
||||
self.game.aircraft_inventory,
|
||||
[cp for cp in game.theater.controlpoints if cp.captured],
|
||||
self.aircraft_selector.currentData()
|
||||
)
|
||||
layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector))
|
||||
|
||||
self.flight_size_spinner = QFlightSizeSpinner()
|
||||
layout.addLayout(QLabeledWidget("Count:", self.flight_size_spinner))
|
||||
|
||||
layout.addLayout(type_layout)
|
||||
layout.addLayout(count_layout)
|
||||
layout.addLayout(flight_type_layout)
|
||||
layout.addStretch()
|
||||
layout.addWidget(self.add_button, alignment=Qt.AlignRight)
|
||||
|
||||
self.create_button = QPushButton("Create")
|
||||
self.create_button.clicked.connect(self.create_flight)
|
||||
layout.addWidget(self.create_button, alignment=Qt.AlignRight)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def create_flight(self):
|
||||
aircraft_type = self.select_type_aircraft.currentData()
|
||||
count = self.select_count_of_aircraft.value()
|
||||
def verify_form(self) -> Optional[str]:
|
||||
aircraft: PlaneType = self.aircraft_selector.currentData()
|
||||
origin: ControlPoint = self.airfield_selector.currentData()
|
||||
size: int = self.flight_size_spinner.value()
|
||||
if not origin.captured:
|
||||
return f"{origin.name} is not owned by your coalition."
|
||||
available = origin.base.aircraft.get(aircraft, 0)
|
||||
if not available:
|
||||
return f"{origin.name} has no {aircraft.id} available."
|
||||
if size > available:
|
||||
return f"{origin.name} has only {available} {aircraft.id} available."
|
||||
return None
|
||||
|
||||
if self.available[aircraft_type] < count:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Information)
|
||||
msg.setText("Not enough aircraft of this type are available. Only " + str(self.available[aircraft_type]) + " available.")
|
||||
msg.setWindowTitle("Not enough aircraft")
|
||||
msg.setStandardButtons(QMessageBox.Ok)
|
||||
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
msg.exec_()
|
||||
def create_flight(self) -> None:
|
||||
error = self.verify_form()
|
||||
if error is not None:
|
||||
self.error_box("Could not create flight", error)
|
||||
return
|
||||
else:
|
||||
flight = Flight(aircraft_type, count, self.from_cp, self.select_flight_type.currentData())
|
||||
self.planner.flights.append(flight)
|
||||
self.planner.custom_flights.append(flight)
|
||||
if self.flight_view is not None:
|
||||
self.flight_view.set_flight_planner(self.planner, len(self.planner.flights)-1)
|
||||
self.close()
|
||||
|
||||
task = self.task_selector.currentData()
|
||||
aircraft = self.aircraft_selector.currentData()
|
||||
origin = self.airfield_selector.currentData()
|
||||
size = self.flight_size_spinner.value()
|
||||
|
||||
flight = Flight(aircraft, size, origin, task)
|
||||
self.populate_flight_plan(flight, task)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.created.emit(flight)
|
||||
self.close()
|
||||
|
||||
def on_aircraft_changed(self, index: int) -> None:
|
||||
new_aircraft = self.aircraft_selector.itemData(index)
|
||||
self.airfield_selector.change_aircraft(new_aircraft)
|
||||
|
||||
@property
|
||||
def planner(self) -> FlightPlanner:
|
||||
return self.game.planners[self.airfield_selector.currentData().id]
|
||||
|
||||
def populate_flight_plan(self, flight: Flight, task: FlightType) -> None:
|
||||
# TODO: Flesh out mission types.
|
||||
if task == FlightType.ANTISHIP:
|
||||
logging.error("Anti-ship flight plan generation not implemented")
|
||||
elif task == FlightType.BAI:
|
||||
logging.error("BAI flight plan generation not implemented")
|
||||
elif task == FlightType.BARCAP:
|
||||
self.generate_cap(flight)
|
||||
elif task == FlightType.CAP:
|
||||
self.generate_cap(flight)
|
||||
elif task == FlightType.CAS:
|
||||
self.generate_cas(flight)
|
||||
elif task == FlightType.DEAD:
|
||||
self.generate_sead(flight)
|
||||
elif task == FlightType.ELINT:
|
||||
logging.error("ELINT flight plan generation not implemented")
|
||||
elif task == FlightType.EVAC:
|
||||
logging.error("Evac flight plan generation not implemented")
|
||||
elif task == FlightType.EWAR:
|
||||
logging.error("EWar flight plan generation not implemented")
|
||||
elif task == FlightType.INTERCEPTION:
|
||||
logging.error("Intercept flight plan generation not implemented")
|
||||
elif task == FlightType.LOGISTICS:
|
||||
logging.error("Logistics flight plan generation not implemented")
|
||||
elif task == FlightType.RECON:
|
||||
logging.error("Recon flight plan generation not implemented")
|
||||
elif task == FlightType.SEAD:
|
||||
self.generate_sead(flight)
|
||||
elif task == FlightType.STRIKE:
|
||||
self.generate_strike(flight)
|
||||
elif task == FlightType.TARCAP:
|
||||
self.generate_cap(flight)
|
||||
elif task == FlightType.TROOP_TRANSPORT:
|
||||
logging.error(
|
||||
"Troop transport flight plan generation not implemented"
|
||||
)
|
||||
|
||||
def generate_cas(self, flight: Flight) -> None:
|
||||
if not isinstance(self.package.target, FrontLine):
|
||||
logging.error(
|
||||
"Could not create flight plan: CAS missions only valid for "
|
||||
"front lines"
|
||||
)
|
||||
return
|
||||
self.planner.generate_cas(flight, self.package.target)
|
||||
|
||||
def generate_cap(self, flight: Flight) -> None:
|
||||
if isinstance(self.package.target, TheaterGroundObject):
|
||||
logging.error(
|
||||
"Could not create flight plan: CAP missions for strike targets "
|
||||
"not implemented"
|
||||
)
|
||||
return
|
||||
if isinstance(self.package.target, FrontLine):
|
||||
self.planner.generate_frontline_cap(flight, self.package.target)
|
||||
else:
|
||||
self.planner.generate_barcap(flight, self.package.target)
|
||||
|
||||
def generate_sead(self, flight: Flight) -> None:
|
||||
self.planner.generate_sead(flight, self.package.target)
|
||||
|
||||
def generate_strike(self, flight: Flight) -> None:
|
||||
if not isinstance(self.package.target, TheaterGroundObject):
|
||||
logging.error(
|
||||
"Could not create flight plan: strike missions for capture "
|
||||
"points not implemented"
|
||||
)
|
||||
return
|
||||
self.planner.generate_strike(flight, self.package.target)
|
||||
|
||||
@ -1,42 +1,31 @@
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import QTabWidget, QFrame, QGridLayout, QLabel
|
||||
from PySide2.QtWidgets import QTabWidget
|
||||
|
||||
from gen.flights.flight import Flight
|
||||
from game import Game
|
||||
from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import QFlightPayloadTab
|
||||
from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import QGeneralFlightSettingsTab
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import QFlightWaypointTab
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \
|
||||
QFlightPayloadTab
|
||||
from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import \
|
||||
QGeneralFlightSettingsTab
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import \
|
||||
QFlightWaypointTab
|
||||
|
||||
|
||||
class QFlightPlanner(QTabWidget):
|
||||
|
||||
on_planned_flight_changed = Signal()
|
||||
|
||||
def __init__(self, flight: Flight, game: Game, planner, selected_tab):
|
||||
super(QFlightPlanner, self).__init__()
|
||||
def __init__(self, flight: Flight, game: Game):
|
||||
super().__init__()
|
||||
|
||||
print(selected_tab)
|
||||
|
||||
self.tabCount = 0
|
||||
if flight:
|
||||
self.general_settings_tab = QGeneralFlightSettingsTab(flight, game, planner)
|
||||
self.general_settings_tab.on_flight_settings_changed.connect(lambda: self.on_planned_flight_changed.emit())
|
||||
self.payload_tab = QFlightPayloadTab(flight, game)
|
||||
self.waypoint_tab = QFlightWaypointTab(game, flight)
|
||||
self.waypoint_tab.on_flight_changed.connect(lambda: self.on_planned_flight_changed.emit())
|
||||
self.addTab(self.general_settings_tab, "General Flight settings")
|
||||
self.addTab(self.payload_tab, "Payload")
|
||||
self.addTab(self.waypoint_tab, "Waypoints")
|
||||
self.tabCount = 3
|
||||
self.setCurrentIndex(selected_tab)
|
||||
else:
|
||||
tabError = QFrame()
|
||||
l = QGridLayout()
|
||||
l.addWidget(QLabel("No flight selected"))
|
||||
tabError.setLayout(l)
|
||||
self.addTab(tabError, "No flight")
|
||||
self.tabCount = 1
|
||||
|
||||
def clearTabs(self):
|
||||
for i in range(self.tabCount):
|
||||
self.removeTab(i)
|
||||
self.general_settings_tab = QGeneralFlightSettingsTab(game, flight)
|
||||
self.general_settings_tab.on_flight_settings_changed.connect(
|
||||
lambda: self.on_planned_flight_changed.emit())
|
||||
self.payload_tab = QFlightPayloadTab(flight, game)
|
||||
self.waypoint_tab = QFlightWaypointTab(game, flight)
|
||||
self.waypoint_tab.on_flight_changed.connect(
|
||||
lambda: self.on_planned_flight_changed.emit())
|
||||
self.addTab(self.general_settings_tab, "General Flight settings")
|
||||
self.addTab(self.payload_tab, "Payload")
|
||||
self.addTab(self.waypoint_tab, "Waypoints")
|
||||
self.setCurrentIndex(0)
|
||||
|
||||
@ -6,12 +6,14 @@ class QFlightSlotEditor(QGroupBox):
|
||||
|
||||
changed = Signal()
|
||||
|
||||
def __init__(self, flight, game, planner):
|
||||
def __init__(self, flight, game):
|
||||
super(QFlightSlotEditor, self).__init__("Slots")
|
||||
self.flight = flight
|
||||
self.game = game
|
||||
self.planner = planner
|
||||
self.available = self.planner.get_available_aircraft()
|
||||
inventory = self.game.aircraft_inventory.for_control_point(
|
||||
flight.from_cp
|
||||
)
|
||||
self.available = inventory.all_aircraft
|
||||
if self.flight.unit_type not in self.available:
|
||||
max = self.flight.count
|
||||
else:
|
||||
|
||||
@ -12,18 +12,15 @@ from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import QFlightTyp
|
||||
class QGeneralFlightSettingsTab(QFrame):
|
||||
on_flight_settings_changed = Signal()
|
||||
|
||||
def __init__(self, flight: Flight, game: Game, planner):
|
||||
def __init__(self, game: Game, flight: Flight):
|
||||
super(QGeneralFlightSettingsTab, self).__init__()
|
||||
self.flight = flight
|
||||
self.game = game
|
||||
self.planner = planner
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
layout = QGridLayout()
|
||||
flight_info = QFlightTypeTaskInfo(self.flight)
|
||||
flight_departure = QFlightDepartureEditor(self.flight)
|
||||
flight_slots = QFlightSlotEditor(self.flight, self.game, self.planner)
|
||||
flight_slots = QFlightSlotEditor(self.flight, self.game)
|
||||
flight_start_type = QFlightStartType(self.flight)
|
||||
layout.addWidget(flight_info, 0, 0)
|
||||
layout.addWidget(flight_departure, 1, 0)
|
||||
@ -35,5 +32,7 @@ class QGeneralFlightSettingsTab(QFrame):
|
||||
self.setLayout(layout)
|
||||
|
||||
flight_start_type.setEnabled(self.flight.client_count > 0)
|
||||
flight_slots.changed.connect(lambda: flight_start_type.setEnabled(self.flight.client_count > 0))
|
||||
flight_departure.changed.connect(lambda: self.on_flight_settings_changed.emit())
|
||||
flight_slots.changed.connect(
|
||||
lambda: flight_start_type.setEnabled(self.flight.client_count > 0))
|
||||
flight_departure.changed.connect(
|
||||
lambda: self.on_flight_settings_changed.emit())
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from .controlpoint import *
|
||||
from .conflicttheater import *
|
||||
from .base import *
|
||||
from .conflicttheater import *
|
||||
from .controlpoint import *
|
||||
from .frontline import FrontLine
|
||||
from .missiontarget import MissionTarget
|
||||
|
||||
@ -46,7 +46,7 @@ COAST_DR_W = [135, 180, 225, 315]
|
||||
|
||||
class ConflictTheater:
|
||||
terrain = None # type: dcs.terrain.Terrain
|
||||
controlpoints = None # type: typing.Collection[ControlPoint]
|
||||
controlpoints = None # type: typing.List[ControlPoint]
|
||||
|
||||
reference_points = None # type: typing.Dict
|
||||
overview_image = None # type: str
|
||||
|
||||
@ -3,11 +3,17 @@ import typing
|
||||
from enum import Enum
|
||||
|
||||
from dcs.mapping import *
|
||||
from dcs.terrain import Airport
|
||||
from dcs.ships import CVN_74_John_C__Stennis, LHA_1_Tarawa, CV_1143_5_Admiral_Kuznetsov, Type_071_Amphibious_Transport_Dock
|
||||
from dcs.ships import (
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
LHA_1_Tarawa,
|
||||
Type_071_Amphibious_Transport_Dock,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport
|
||||
|
||||
from game import db
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import TheaterGroundObject
|
||||
|
||||
|
||||
@ -19,7 +25,7 @@ class ControlPointType(Enum):
|
||||
FOB = 5 # A FOB (ground units only)
|
||||
|
||||
|
||||
class ControlPoint:
|
||||
class ControlPoint(MissionTarget):
|
||||
|
||||
id = 0
|
||||
position = None # type: Point
|
||||
@ -206,4 +212,3 @@ class ControlPoint:
|
||||
if g.obj_name == obj_name:
|
||||
found.append(g)
|
||||
return found
|
||||
|
||||
|
||||
27
theater/frontline.py
Normal file
27
theater/frontline.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Battlefield front lines."""
|
||||
from typing import Tuple
|
||||
|
||||
from . import ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class FrontLine(MissionTarget):
|
||||
"""Defines a front line location between two control points.
|
||||
|
||||
Front lines are the area where ground combat happens.
|
||||
"""
|
||||
|
||||
def __init__(self, control_point_a: ControlPoint,
|
||||
control_point_b: ControlPoint) -> None:
|
||||
self.control_point_a = control_point_a
|
||||
self.control_point_b = control_point_b
|
||||
|
||||
@property
|
||||
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||
"""Returns a tuple of the two control points."""
|
||||
return self.control_point_a, self.control_point_b
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
a = self.control_point_a.name
|
||||
b = self.control_point_b.name
|
||||
return f"Front line {a}/{b}"
|
||||
11
theater/missiontarget.py
Normal file
11
theater/missiontarget.py
Normal file
@ -0,0 +1,11 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class MissionTarget(ABC):
|
||||
# TODO: These should just be required objects to the constructor
|
||||
# The TheatherGroundObject class is difficult to modify because it's
|
||||
# generated data that's pickled ahead of time.
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""The name of the mission target."""
|
||||
@ -1,6 +1,9 @@
|
||||
from dcs.mapping import Point
|
||||
import uuid
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from .missiontarget import MissionTarget
|
||||
|
||||
NAME_BY_CATEGORY = {
|
||||
"power": "Power plant",
|
||||
"ammo": "Ammo depot",
|
||||
@ -59,7 +62,7 @@ CATEGORY_MAP = {
|
||||
}
|
||||
|
||||
|
||||
class TheaterGroundObject:
|
||||
class TheaterGroundObject(MissionTarget):
|
||||
cp_id = 0
|
||||
group_id = 0
|
||||
object_id = 0
|
||||
@ -93,3 +96,15 @@ class TheaterGroundObject:
|
||||
|
||||
def matches_string_identifier(self, id):
|
||||
return self.string_identifier == id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.obj_name
|
||||
|
||||
def parent_control_point(
|
||||
self, theater: "ConflictTheater") -> "ControlPoint":
|
||||
"""Searches the theater for the parent control point."""
|
||||
for cp in theater.controlpoints:
|
||||
if cp.id == self.cp_id:
|
||||
return cp
|
||||
raise RuntimeError("Could not find matching control point in theater")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user