Merge branch 'develop' into frontline

This commit is contained in:
walterroach 2020-11-21 11:19:31 -06:00
commit 316f73138c
14 changed files with 106 additions and 85 deletions

View File

@ -5,8 +5,7 @@
* **[Flight Planner]** Added BAI missions.
* **[Flight Planner]** Added anti-ship missions.
* **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package.
* **[Modding]** Possible to setup liveries overrides for factions
* **[Units]** Added support for F-14A-135-GR
* **[Culling]** Added possibility to include/exclude carriers from culling zones
# 2.2.1
@ -15,13 +14,17 @@
* **[Factions]** Added map Persian Gulf full by Plob
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.
* **[UI]** Mission start screen now informs players about delayed flights.
* **[Units]** Added support for F-14A-135-GR
* **[Modding]** Possible to setup liveries overrides in factions definition files
## Fixes :
* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned.
* **[Flight Planner]** Spitfire clipped wing variant was not seen as a flyable module
* **[Flight Planner]** Custom waypoints are usable again. Not that in most cases custom flight plans will revert to the 2.1 flight planning behavior.
* **[Flight Planner]** Fixed UI bug that made it possible to create empty flights which would throw an error.
* **[Flight Planner]** Player flights from carriers will now be delayed correctly according to the player's settings.
* **[Misc]** Spitfire variant with clipped wings was not seen as flyable by DCS Liberation (hence could not be setup as client/player slot)
* **[Misc]** Updated Syria terrain parking slots database, the out-of-date database could end up generating aircraft in wrong slots (We are still experiencing issues with somes airbases, such as Khalkhalah though)
# 2.2.0

View File

@ -1244,7 +1244,7 @@ def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id
def unit_type_from_name(name: str) -> Optional[UnitType]:
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
if name in vehicle_map:
return vehicle_map[name]
elif name in plane_map:

View File

@ -107,14 +107,16 @@ class Event:
for destroyed_aircraft in debriefing.killed_aircrafts:
try:
cpid = int(destroyed_aircraft.split("|")[3])
type = db.unit_type_from_name(destroyed_aircraft.split("|")[4])
if cpid in cp_map.keys():
aircraft = db.unit_type_from_name(
destroyed_aircraft.split("|")[4])
if cpid in cp_map:
cp = cp_map[cpid]
if type in cp.base.aircraft.keys():
logging.info("Aircraft destroyed : " + str(type))
cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1)
except Exception as e:
print(e)
if aircraft in cp.base.aircraft:
logging.info(f"Aircraft destroyed: {aircraft}")
cp.base.aircraft[aircraft] = max(
0, cp.base.aircraft[aircraft] - 1)
except Exception:
logging.exception("Failed to commit destroyed aircraft")
# ------------------------------
# Destroyed ground units
@ -123,13 +125,13 @@ class Event:
for killed_ground_unit in debriefing.killed_ground_units:
try:
cpid = int(killed_ground_unit.split("|")[3])
type = db.unit_type_from_name(killed_ground_unit.split("|")[4])
aircraft = db.unit_type_from_name(killed_ground_unit.split("|")[4])
if cpid in cp_map.keys():
killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1
cp = cp_map[cpid]
if type in cp.base.armor.keys():
logging.info("Ground unit destroyed : " + str(type))
cp.base.armor[type] = max(0, cp.base.armor[type] - 1)
if aircraft in cp.base.armor.keys():
logging.info("Ground unit destroyed : " + str(aircraft))
cp.base.armor[aircraft] = max(0, cp.base.armor[aircraft] - 1)
except Exception as e:
print(e)
@ -352,11 +354,13 @@ class Event:
logging.info(info.text)
class UnitsDeliveryEvent(Event):
informational = True
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
def __init__(self, attacker_name: str, defender_name: str,
from_cp: ControlPoint, to_cp: ControlPoint,
game: Game) -> None:
super(UnitsDeliveryEvent, self).__init__(game=game,
location=to_cp.position,
from_cp=from_cp,
@ -364,17 +368,16 @@ class UnitsDeliveryEvent(Event):
attacker_name=attacker_name,
defender_name=defender_name)
self.units: Dict[UnitType, int] = {}
self.units: Dict[Type[UnitType], int] = {}
def __str__(self):
def __str__(self) -> str:
return "Pending delivery to {}".format(self.to_cp)
def deliver(self, units: Dict[UnitType, int]):
def deliver(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] = self.units.get(k, 0) + v
def skip(self):
def skip(self) -> None:
for k, v in self.units.items():
info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn)
self.game.informations.append(info)

View File

@ -84,7 +84,8 @@ class Game:
self.ground_planners: Dict[int, GroundPlanner] = {}
self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0))
self.__culling_points = self.compute_conflicts_position()
self.__culling_points: List[Point] = []
self.compute_conflicts_position()
self.__destroyed_units: List[str] = []
self.savepath = ""
self.budget = PLAYER_BUDGET_INITIAL
@ -239,7 +240,7 @@ class Game:
self.aircraft_inventory.set_from_control_point(cp)
# Plan flights & combat for next turn
self.__culling_points = self.compute_conflicts_position()
self.compute_conflicts_position()
self.ground_planners = {}
self.blue_ato.clear()
self.red_ato.clear()
@ -316,7 +317,7 @@ class Game:
if i > 50 or budget_for_aircraft <= 0:
break
target_cp = random.choice(potential_cp_armor)
if target_cp.base.total_planes >= MAX_AIRCRAFT:
if target_cp.base.total_aircraft >= MAX_AIRCRAFT:
continue
unit = random.choice(potential_units)
price = db.PRICES[unit] * 2
@ -364,6 +365,12 @@ class Game:
points.append(front_line.control_point_a.position)
points.append(front_line.control_point_b.position)
# If do_not_cull_carrier is enabled, add carriers as culling point
if self.settings.perf_do_not_cull_carrier:
for cp in self.theater.controlpoints:
if cp.is_carrier or cp.is_lha:
points.append(cp.position)
# If there is no conflict take the center point between the two nearest opposing bases
if len(points) == 0:
cpoint = None
@ -387,7 +394,7 @@ class Game:
if len(points) == 0:
points.append(Point(0, 0))
return points
self.__culling_points = points
def add_destroyed_units(self, data):
pos = Point(data["x"], data["z"])

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from collections import defaultdict
from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING
from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
from dcs.unittype import FlyingType
@ -17,9 +17,9 @@ class ControlPointAircraftInventory:
def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point
self.inventory: Dict[FlyingType, int] = defaultdict(int)
self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
def add_aircraft(self, aircraft: FlyingType, count: int) -> None:
def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
"""Adds aircraft to the inventory.
Args:
@ -28,7 +28,7 @@ class ControlPointAircraftInventory:
"""
self.inventory[aircraft] += count
def remove_aircraft(self, aircraft: FlyingType, count: int) -> None:
def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
"""Removes aircraft from the inventory.
Args:
@ -47,7 +47,7 @@ class ControlPointAircraftInventory:
)
self.inventory[aircraft] -= count
def available(self, aircraft: FlyingType) -> int:
def available(self, aircraft: Type[FlyingType]) -> int:
"""Returns the number of available aircraft of the given type.
Args:

View File

@ -39,6 +39,7 @@ class Settings:
# Performance culling
perf_culling: bool = False
perf_culling_distance: int = 100
perf_do_not_cull_carrier = True
# LUA Plugins system
plugins: Dict[str, bool] = field(default_factory=dict)

View File

@ -4,9 +4,8 @@ import math
import typing
from typing import Dict, Type
from dcs.planes import PlaneType
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
from dcs.unittype import UnitType, VehicleType
from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.vehicles import AirDefence, Armor
from game import db
@ -21,20 +20,16 @@ BASE_MIN_STRENGTH = 0
class Base:
aircraft = {} # type: typing.Dict[PlaneType, int]
armor = {} # type: typing.Dict[VehicleType, int]
aa = {} # type: typing.Dict[AirDefence, int]
strength = 1 # type: float
def __init__(self):
self.aircraft = {}
self.armor = {}
self.aa = {}
self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: Dict[VehicleType, int] = {}
self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {}
self.strength = 1
@property
def total_planes(self) -> int:
def total_aircraft(self) -> int:
return sum(self.aircraft.values())
@property
@ -83,7 +78,7 @@ class Base:
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
return result
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]:
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]:
return self._find_best_unit(self.aircraft, for_type, count)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
@ -155,7 +150,7 @@ class Base:
if task:
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
else:
count = self.total_planes
count = self.total_aircraft
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
@ -167,18 +162,18 @@ class Base:
# previous logic removed because we always want the full air defense capabilities.
return self.total_aa
def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]:
def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def scramble_last_defense(self):
# return as many CAP-capable aircraft as we can since this is the last defense of the base
# (but not more than 20 - that's just nuts)
return self._find_best_planes(CAP, min(self.total_planes, 20))
return self._find_best_planes(CAP, min(self.total_aircraft, 20))
def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]:
def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]:
def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def assemble_attack(self) -> typing.Dict[Armor, int]:

View File

@ -235,7 +235,7 @@ class ControlPoint(MissionTarget):
return result
@property
def available_aircraft_slots(self):
def total_aircraft_parking(self):
"""
:return: The maximum number of aircraft that can be stored in this control point
"""
@ -376,6 +376,19 @@ class ControlPoint(MissionTarget):
return False
return True
@property
def expected_aircraft_next_turn(self) -> int:
total = self.base.total_aircraft
assert self.pending_unit_deliveries
for unit_bought in self.pending_unit_deliveries.units:
if issubclass(unit_bought, FlyingType):
total += self.pending_unit_deliveries.units[unit_bought]
return total
@property
def unclaimed_parking(self) -> int:
return self.total_aircraft_parking - self.expected_aircraft_next_turn
class OffMapSpawn(ControlPoint):
def __init__(self, id: int, name: str, position: Point):
@ -391,5 +404,5 @@ class OffMapSpawn(ControlPoint):
yield from []
@property
def available_aircraft_slots(self) -> int:
def total_aircraft_parking(self) -> int:
return 1000

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import timedelta
from enum import Enum
from typing import Dict, List, Optional, TYPE_CHECKING
from typing import Dict, List, Optional, TYPE_CHECKING, Type
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
@ -133,8 +133,8 @@ class FlightWaypoint:
class Flight:
def __init__(self, package: Package, unit_type: FlyingType, count: int,
flight_type: FlightType, start_type: str,
def __init__(self, package: Package, unit_type: Type[FlyingType],
count: int, flight_type: FlightType, start_type: str,
departure: ControlPoint, arrival: ControlPoint,
divert: Optional[ControlPoint]) -> None:
self.package = package

View File

@ -296,7 +296,7 @@ class QLiberationMap(QGraphicsView):
# Display Culling
if DisplayOptions.culling and self.game.settings.perf_culling:
self.display_culling()
self.display_culling(scene)
for cp in self.game.theater.controlpoints:

View File

@ -57,7 +57,7 @@ class QBaseMenu2(QDialog):
title = QLabel("<b>" + self.cp.name + "</b>")
title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title")
unitsPower = QLabel("{} / {} / Runway : {}".format(self.cp.base.total_planes, self.cp.base.total_armor,
unitsPower = QLabel("{} / {} / Runway : {}".format(self.cp.base.total_aircraft, self.cp.base.total_armor,
"Available" if self.cp.has_runway() else "Unavailable"))
self.topLayout.addWidget(title)
self.topLayout.addWidget(unitsPower)

View File

@ -1,4 +1,5 @@
import logging
from typing import Type
from PySide2.QtWidgets import (
QGroupBox,
@ -106,7 +107,7 @@ class QRecruitBehaviour:
return row + 1
def _update_count_label(self, unit_type: UnitType):
def _update_count_label(self, unit_type: Type[UnitType]):
self.bought_amount_labels[unit_type].setText("<b>{}</b>".format(
unit_type in self.pending_deliveries.units and "{}".format(self.pending_deliveries.units[unit_type]) or "0"
@ -125,14 +126,7 @@ class QRecruitBehaviour:
child.setText(
QRecruitBehaviour.BUDGET_FORMAT.format(self.budget))
def buy(self, unit_type):
if self.maximum_units > 0:
if self.total_units + 1 > self.maximum_units:
logging.info("Not enough space left !")
# TODO : display modal warning
return
def buy(self, unit_type: Type[UnitType]):
price = db.PRICES[unit_type]
if self.budget >= price:
self.pending_deliveries.deliver({unit_type: 1})
@ -158,19 +152,6 @@ class QRecruitBehaviour:
self._update_count_label(unit_type)
self.update_available_budget()
@property
def total_units(self):
total = 0
for unit_type in self.recruitables_types:
total += self.cp.base.total_units(unit_type)
if self.pending_deliveries:
for unit_bought in self.pending_deliveries.units:
if db.unit_task(unit_bought) in self.recruitables_types:
total += self.pending_deliveries.units[unit_bought]
return total
def set_maximum_units(self, maximum_units):
"""
Set the maximum number of units that can be bought

View File

@ -1,3 +1,4 @@
import logging
from typing import Optional, Set
from PySide2.QtCore import Qt
@ -31,13 +32,13 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
self.existing_units_labels = {}
# Determine maximum number of aircrafts that can be bought
self.set_maximum_units(self.cp.available_aircraft_slots)
self.set_maximum_units(self.cp.total_aircraft_parking)
self.set_recruitable_types([CAP, CAS])
self.bought_amount_labels = {}
self.existing_units_labels = {}
self.hangar_status = QHangarStatus(self.total_units, self.cp.available_aircraft_slots)
self.hangar_status = QHangarStatus(self.cp)
self.init_ui()
@ -80,8 +81,13 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
self.setLayout(main_layout)
def buy(self, unit_type):
if self.maximum_units > 0:
if self.cp.unclaimed_parking <= 0:
logging.debug(f"No space for additional aircraft at {self.cp}.")
return
super().buy(unit_type)
self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
self.hangar_status.update_label()
def sell(self, unit_type: UnitType):
# Don't need to remove aircraft from the inventory if we're canceling
@ -99,22 +105,26 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
"assigned to a mission?", QMessageBox.Ok)
return
super().sell(unit_type)
self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
self.hangar_status.update_label()
class QHangarStatus(QHBoxLayout):
def __init__(self, current_amount: int, max_amount: int):
super(QHangarStatus, self).__init__()
def __init__(self, control_point: ControlPoint) -> None:
super().__init__()
self.control_point = control_point
self.icon = QLabel()
self.icon.setPixmap(ICONS["Hangar"])
self.text = QLabel("")
self.update_label(current_amount, max_amount)
self.update_label()
self.addWidget(self.icon, Qt.AlignLeft)
self.addWidget(self.text, Qt.AlignLeft)
self.addStretch(50)
self.setAlignment(Qt.AlignLeft)
def update_label(self, current_amount: int, max_amount: int):
self.text.setText("<strong>{}/{}</strong>".format(current_amount, max_amount))
def update_label(self) -> None:
current_amount = self.control_point.expected_aircraft_next_turn
max_amount = self.control_point.total_aircraft_parking
self.text.setText(f"<strong>{current_amount}/{max_amount}</strong>")

View File

@ -279,6 +279,10 @@ class QSettingsWindow(QDialog):
self.culling_distance.setValue(self.game.settings.perf_culling_distance)
self.culling_distance.valueChanged.connect(self.applySettings)
self.culling_do_not_cull_carrier = QCheckBox()
self.culling_do_not_cull_carrier.setChecked(self.game.settings.perf_do_not_cull_carrier)
self.culling_do_not_cull_carrier.toggled.connect(self.applySettings)
self.performanceLayout.addWidget(QLabel("Smoke visual effect on frontline"), 0, 0)
self.performanceLayout.addWidget(self.smoke, 0, 1, alignment=Qt.AlignRight)
self.performanceLayout.addWidget(QLabel("SAM starts in RED alert mode"), 1, 0)
@ -299,6 +303,8 @@ class QSettingsWindow(QDialog):
self.performanceLayout.addWidget(self.culling, 8, 1, alignment=Qt.AlignRight)
self.performanceLayout.addWidget(QLabel("Culling distance (km)"), 9, 0)
self.performanceLayout.addWidget(self.culling_distance, 9, 1, alignment=Qt.AlignRight)
self.performanceLayout.addWidget(QLabel("Do not cull carrier's surroundings"), 10, 0)
self.performanceLayout.addWidget(self.culling_do_not_cull_carrier, 10, 1, alignment=Qt.AlignRight)
self.generatorLayout.addWidget(self.gameplay)
self.generatorLayout.addWidget(QLabel("Disabling settings below may improve performance, but will impact the overall quality of the experience."))
@ -366,9 +372,11 @@ class QSettingsWindow(QDialog):
self.game.settings.perf_culling = self.culling.isChecked()
self.game.settings.perf_culling_distance = int(self.culling_distance.value())
self.game.settings.perf_do_not_cull_carrier = self.culling_do_not_cull_carrier.isChecked()
self.game.settings.show_red_ato = self.cheat_options.show_red_ato
self.game.compute_conflicts_position()
GameUpdateSignal.get_instance().updateGame(self.game)
def onSelectionChanged(self):