Compare commits

..

16 Commits
2.4.0 ... 2.4.2

Author SHA1 Message Date
Dan Albert
e40b916b07 Merge pull request #884 from Khopa/develop_2_4_x
Release 2.4.2.
2021-02-12 19:05:44 -08:00
Dan Albert
1bc994c102 Fix version number of the release. 2021-02-12 16:18:01 -08:00
Dan Albert
21be4d38e1 Mention new start dates in the changelog.
(cherry picked from commit f7889b785d)
2021-02-12 16:16:36 -08:00
Mustang-25
3b58c571b3 Add a mid-90s campaign date option
1995 is a good date to pick if you want to date restrict all GPS weapons but still have all the laser guided options.

(cherry picked from commit 27829a024a)
2021-02-12 16:16:35 -08:00
Mustang-25
e0430cf607 Adjust HARM Weapon Restriction Date
Official Navy docs have the 88A's IOC date in 1983. Also left a note on the B and C IOC dates if DCS ever adds the older models.

(cherry picked from commit a0fda2552f)
2021-02-12 16:16:31 -08:00
Dan Albert
fb425d3524 Fix rounding of budget in recruitment menu.
Fixes https://github.com/Khopa/dcs_liberation/issues/861.

(cherry picked from commit 5792eb354c)
2021-02-12 14:01:34 -08:00
Dan Albert
45300b64c5 Mention weapon data in changelog.
(cherry picked from commit dce7d91511)
2021-02-12 13:54:51 -08:00
Brandon Danyluk
4b7ef46f82 Add weapon era restrictions for USA/Russia/UK/France (#860)
(cherry picked from commit 61f1e11a48)
2021-02-12 13:49:40 -08:00
Dan Albert
b67e6d20f1 Move 2.4.2 fix to the correct section.
(cherry picked from commit 3d1afa74d4)
2021-02-12 13:48:02 -08:00
Dan Albert
0ae6575087 Transfer pending purchases forward along capture.
Fixes https://github.com/Khopa/dcs_liberation/issues/828.

(cherry picked from commit d8c94f5ece)
2021-02-12 13:46:08 -08:00
Malakhit
08f67860be Merge pull request #854 from Khopa/develop_2_4_x
2.4.1 Release
2021-02-07 11:22:38 +00:00
Simon Clark
3fab1d92b7 Fixed some areas where the non-pretty name for a unit was displayed. 2021-02-07 11:15:25 +00:00
Simon Clark
6573157112 Changelog. 2021-02-07 10:49:34 +00:00
Simon Clark
e83841eb0b Fix syntax error with SH-60B payload. 2021-02-07 10:47:36 +00:00
Simon Clark
6a0e18c0e9 Change the logic for culling missile sites.
Missile sites now generate a 2.5km culling circle around themselves, rather than using the standard full culling zone size.

Fixes #850.
2021-02-06 22:20:02 +00:00
Simon Clark
b71b6473e3 Start 2.4.1, fix #852. 2021-02-06 19:56:17 +00:00
14 changed files with 1020 additions and 228 deletions

View File

@@ -1,3 +1,24 @@
# 2.4.2
## Features/Improvements
* **[Factions]** Introduction dates and fallback weapons added for US, Russian, UK, and French weapons. Huge thanks to @TheCandianVendingMachine for the massive amount of data entry!
* **[Campaigns]** Added 1995 start dates.
## Fixes
* **[Economy]** Pending ground unit purchases will also be transferred when a connected base is captured.
* **[UI]** Fixed rounding of budget in recruitment menu.
# 2.4.1
## Fixes
* **[Units]** Fixed syntax error with the SH-60B payload file.
* **[Culling]** Missile sites generate reasonably sized non-cull zones rather than 100km ones.
* **[UI]** Budget display is also now rounded to 2 decimal places.
* **[UI]** Fixed some areas where the old, non-pretty name was displayed to users.
# 2.4.0 # 2.4.0
Saves from 2.3 are not compatible with 2.4. Saves from 2.3 are not compatible with 2.4.

File diff suppressed because it is too large Load Diff

View File

@@ -1300,6 +1300,10 @@ TIME_PERIODS = {
"Gulf War - Winter [1990]": datetime(1990, 1, 1), "Gulf War - Winter [1990]": datetime(1990, 1, 1),
"Gulf War - Spring [1990]": datetime(1990, 4, 1), "Gulf War - Spring [1990]": datetime(1990, 4, 1),
"Gulf War - Summer [1990]": datetime(1990, 6, 1), "Gulf War - Summer [1990]": datetime(1990, 6, 1),
"Mid-90s - Winter [1995]": datetime(1995, 1, 1),
"Mid-90s - Spring [1995]": datetime(1995, 4, 1),
"Mid-90s - Summer [1995]": datetime(1995, 6, 1),
"Mid-90s - Fall [1995]": datetime(1995, 10, 1),
"Gulf War - Fall [1990]": datetime(1990, 10, 1), "Gulf War - Fall [1990]": datetime(1990, 10, 1),
"Modern - Winter [2010]": datetime(2010, 1, 1), "Modern - Winter [2010]": datetime(2010, 1, 1),
"Modern - Spring [2010]": datetime(2010, 4, 1), "Modern - Spring [2010]": datetime(2010, 4, 1),
@@ -1309,7 +1313,7 @@ TIME_PERIODS = {
"Syrian War [2011]": datetime(2011, 3, 15), "Syrian War [2011]": datetime(2011, 3, 15),
"6 days war [1967]": datetime(1967, 6, 5), "6 days war [1967]": datetime(1967, 6, 5),
"Yom Kippour War [1973]": datetime(1973, 10, 6), "Yom Kippour War [1973]": datetime(1973, 10, 6),
"Lebanon War [1982]": datetime(1982, 6, 6), "First Lebanon War [1982]": datetime(1982, 6, 6),
"Arab-Israeli War [1948]": datetime(1948, 5, 15), "Arab-Israeli War [1948]": datetime(1948, 5, 15),
} }

View File

@@ -6,7 +6,7 @@ from typing import Dict, Iterator, List, TYPE_CHECKING, Tuple, Type
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import Task from dcs.task import Task
from dcs.unittype import UnitType from dcs.unittype import UnitType, VehicleType
from game import persistency from game import persistency
from game.debriefing import AirLosses, Debriefing from game.debriefing import AirLosses, Debriefing
@@ -296,45 +296,69 @@ class Event:
self.game.turn) self.game.turn)
self.game.informations.append(info) self.game.informations.append(info)
def redeploy_units(self, cp): def redeploy_units(self, cp: ControlPoint) -> None:
"""" """"
Auto redeploy units to newly captured base Auto redeploy units to newly captured base
""" """
ally_connected_cps = [ocp for ocp in cp.connected_points if cp.captured == ocp.captured] ally_connected_cps = [ocp for ocp in cp.connected_points if
enemy_connected_cps = [ocp for ocp in cp.connected_points if cp.captured != ocp.captured] cp.captured == ocp.captured]
enemy_connected_cps = [ocp for ocp in cp.connected_points if
cp.captured != ocp.captured]
# If the newly captured cp does not have enemy connected cp, # If the newly captured cp does not have enemy connected cp,
# then it is not necessary to redeploy frontline units there. # then it is not necessary to redeploy frontline units there.
if len(enemy_connected_cps) == 0: if len(enemy_connected_cps) == 0:
return return
# From each ally cp, send reinforcements
for ally_cp in ally_connected_cps:
self.redeploy_between(cp, ally_cp)
def redeploy_between(self, destination: ControlPoint,
source: ControlPoint) -> None:
total_units_redeployed = 0
moved_units = {}
if source.has_active_frontline or not destination.captured:
# If there are still active front lines to defend at the
# transferring CP we should not transfer all units.
#
# Opfor also does not transfer all of their units.
# TODO: Balance the CPs rather than moving half from everywhere.
move_factor = 0.5
else: else:
# From each ally cp, send reinforcements # Otherwise we can move everything.
for ally_cp in ally_connected_cps: move_factor = 1
total_units_redeployed = 0
own_enemy_cp = [ocp for ocp in ally_cp.connected_points if ally_cp.captured != ocp.captured]
moved_units = {} for frontline_unit, count in source.base.armor.items():
moved_units[frontline_unit] = int(count * move_factor)
total_units_redeployed = total_units_redeployed + int(
count * move_factor)
# If the connected base, does not have any more enemy cp connected. destination.base.commision_units(moved_units)
# Or if it is not the opponent redeploying forces there (enemy AI will never redeploy all their forces at once) source.base.commit_losses(moved_units)
if len(own_enemy_cp) > 0 or not cp.captured:
for frontline_unit, count in ally_cp.base.armor.items():
moved_units[frontline_unit] = int(count/2)
total_units_redeployed = total_units_redeployed + int(count/2)
else: # So if the old base, does not have any more enemy cp connected, or if it is an enemy base
for frontline_unit, count in ally_cp.base.armor.items():
moved_units[frontline_unit] = count
total_units_redeployed = total_units_redeployed + count
cp.base.commision_units(moved_units) # Also transfer pending deliveries.
ally_cp.base.commit_losses(moved_units) for unit_type, count in source.pending_unit_deliveries.units.items():
if not issubclass(unit_type, VehicleType):
continue
if count <= 0:
# Don't transfer *sales*...
continue
move_count = int(count * move_factor)
source.pending_unit_deliveries.sell({unit_type: move_count})
destination.pending_unit_deliveries.order({unit_type: move_count})
total_units_redeployed += move_count
if total_units_redeployed > 0: if total_units_redeployed > 0:
info = Information("Units redeployed", "", self.game.turn) text = (
info.text = str(total_units_redeployed) + " units have been redeployed from " + ally_cp.name + " to " + cp.name f"{total_units_redeployed} units have been redeployed from "
self.game.informations.append(info) f"{source.name} to {destination.name}"
logging.info(info.text) )
info = Information("Units redeployed", text, self.game.turn)
self.game.informations.append(info)
logging.info(text)
class UnitsDeliveryEvent: class UnitsDeliveryEvent:

View File

@@ -96,6 +96,9 @@ class Game:
self.ground_planners: Dict[int, GroundPlanner] = {} self.ground_planners: Dict[int, GroundPlanner] = {}
self.informations = [] self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0)) self.informations.append(Information("Game Start", "-" * 40, 0))
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
self.__culling_zones: List[Point] = []
# Culling Points are for individual theater ground objects that we don't wish to cull.
self.__culling_points: List[Point] = [] self.__culling_points: List[Point] = []
self.__destroyed_units: List[str] = [] self.__destroyed_units: List[str] = []
self.savepath = "" self.savepath = ""
@@ -378,6 +381,7 @@ class Game:
Compute the current conflict center position(s), mainly used for culling calculation Compute the current conflict center position(s), mainly used for culling calculation
:return: List of points of interests :return: List of points of interests
""" """
zones = []
points = [] points = []
# By default, use the existing frontline conflict position # By default, use the existing frontline conflict position
@@ -385,23 +389,23 @@ class Game:
position = Conflict.frontline_position(front_line.control_point_a, position = Conflict.frontline_position(front_line.control_point_a,
front_line.control_point_b, front_line.control_point_b,
self.theater) self.theater)
points.append(position[0]) zones.append(position[0])
points.append(front_line.control_point_a.position) zones.append(front_line.control_point_a.position)
points.append(front_line.control_point_b.position) zones.append(front_line.control_point_b.position)
for cp in self.theater.controlpoints: for cp in self.theater.controlpoints:
# Don't cull missile sites - their range is long enough to make them # Don't cull missile sites - their range is long enough to make them
# easily culled despite being a threat. # easily culled despite being a threat.
for tgo in cp.ground_objects: for tgo in cp.ground_objects:
if isinstance(tgo, MissileSiteGroundObject): if isinstance(tgo, MissileSiteGroundObject):
points.append(cp.position) points.append(tgo.position)
# If do_not_cull_carrier is enabled, add carriers as culling point # If do_not_cull_carrier is enabled, add carriers as culling point
if self.settings.perf_do_not_cull_carrier: if self.settings.perf_do_not_cull_carrier:
if cp.is_carrier or cp.is_lha: if cp.is_carrier or cp.is_lha:
points.append(cp.position) zones.append(cp.position)
# If there is no conflict take the center point between the two nearest opposing bases # If there is no conflict take the center point between the two nearest opposing bases
if len(points) == 0: if len(zones) == 0:
cpoint = None cpoint = None
min_distance = sys.maxsize min_distance = sys.maxsize
for cp in self.theater.player_points(): for cp in self.theater.player_points():
@@ -410,13 +414,13 @@ class Game:
if d < min_distance: if d < min_distance:
min_distance = d min_distance = d
cpoint = Point((cp.position.x + cp2.position.x) / 2, (cp.position.y + cp2.position.y) / 2) cpoint = Point((cp.position.x + cp2.position.x) / 2, (cp.position.y + cp2.position.y) / 2)
points.append(cp.position) zones.append(cp.position)
points.append(cp2.position) zones.append(cp2.position)
break break
if cpoint is not None: if cpoint is not None:
break break
if cpoint is not None: if cpoint is not None:
points.append(cpoint) zones.append(cpoint)
packages = itertools.chain(self.blue_ato.packages, packages = itertools.chain(self.blue_ato.packages,
self.red_ato.packages) self.red_ato.packages)
@@ -428,13 +432,14 @@ class Game:
# are only interesting if there are enemies in the area, and if # are only interesting if there are enemies in the area, and if
# there are they won't be culled because of the enemy's mission. # there are they won't be culled because of the enemy's mission.
continue continue
points.append(package.target.position) zones.append(package.target.position)
# Else 0,0, since we need a default value # Else 0,0, since we need a default value
# (in this case this means the whole map is owned by the same player, so it is not an issue) # (in this case this means the whole map is owned by the same player, so it is not an issue)
if len(points) == 0: if len(zones) == 0:
points.append(Point(0, 0)) zones.append(Point(0, 0))
self.__culling_zones = zones
self.__culling_points = points self.__culling_points = points
def add_destroyed_units(self, data): def add_destroyed_units(self, data):
@@ -454,11 +459,21 @@ class Game:
if self.settings.perf_culling == False: if self.settings.perf_culling == False:
return False return False
else: else:
for c in self.__culling_points: for z in self.__culling_zones:
if c.distance_to_point(pos) < self.settings.perf_culling_distance * 1000: if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
return False
for p in self.__culling_points:
if p.distance_to_point(pos) < 2500:
return False return False
return True return True
def get_culling_zones(self):
"""
Check culling points
:return: List of culling zones
"""
return self.__culling_zones
def get_culling_points(self): def get_culling_points(self):
""" """
Check culling points Check culling points

View File

@@ -606,6 +606,10 @@ class ControlPoint(MissionTarget, ABC):
def income_per_turn(self) -> int: def income_per_turn(self) -> int:
return 0 return 0
@property
def has_active_frontline(self) -> bool:
return any(
not c.is_friendly(self.captured) for c in self.connected_points)
class Airfield(ControlPoint): class Airfield(ControlPoint):

View File

@@ -2,7 +2,7 @@ from pathlib import Path
def _build_version_string() -> str: def _build_version_string() -> str:
components = ["2.4"] components = ["2.4.2"]
build_number_path = Path("resources/buildnumber") build_number_path = Path("resources/buildnumber")
if build_number_path.exists(): if build_number_path.exists():
with build_number_path.open("r") as build_number_file: with build_number_path.open("r") as build_number_file:

View File

@@ -35,7 +35,7 @@ class QBudgetBox(QGroupBox):
:param budget: Current money available :param budget: Current money available
:param reward: Planned reward for next turn :param reward: Planned reward for next turn
""" """
self.money_amount.setText(str(budget) + "M (+" + str(round(reward,2)) + "M)") self.money_amount.setText(str(round(budget,2)) + "M (+" + str(round(reward,2)) + "M)")
def setGame(self, game): def setGame(self, game):
if game is None: if game is None:

View File

@@ -268,13 +268,20 @@ class QLiberationMap(QGraphicsView):
def display_culling(self, scene: QGraphicsScene) -> None: def display_culling(self, scene: QGraphicsScene) -> None:
"""Draws the culling distance rings on the map""" """Draws the culling distance rings on the map"""
culling_points = self.game_model.game.get_culling_points() culling_points = self.game_model.game.get_culling_points()
culling_zones = self.game_model.game.get_culling_zones()
culling_distance = self.game_model.game.settings.perf_culling_distance culling_distance = self.game_model.game.settings.perf_culling_distance
for point in culling_points: for point in culling_points:
culling_distance_point = Point(point.x + culling_distance*1000, point.y + culling_distance*1000) culling_distance_point = Point(point.x + 2500, point.y + 2500)
distance_point = self._transform_point(culling_distance_point) distance_point = self._transform_point(culling_distance_point)
transformed = self._transform_point(point) transformed = self._transform_point(point)
radius = distance_point[0] - transformed[0] radius = distance_point[0] - transformed[0]
scene.addEllipse(transformed[0]-radius, transformed[1]-radius, 2*radius, 2*radius, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"]) scene.addEllipse(transformed[0]-radius, transformed[1]-radius, 2*radius, 2*radius, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"])
for zone in culling_zones:
culling_distance_zone = Point(zone.x + culling_distance*1000, zone.y + culling_distance*1000)
distance_zone = self._transform_point(culling_distance_zone)
transformed = self._transform_point(zone)
radius = distance_zone[0] - transformed[0]
scene.addEllipse(transformed[0]-radius, transformed[1]-radius, 2*radius, 2*radius, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"])
def draw_shapely_poly(self, scene: QGraphicsScene, poly: Polygon, pen: QPen, def draw_shapely_poly(self, scene: QGraphicsScene, poly: Polygon, pen: QPen,
brush: QBrush) -> Optional[QPolygonF]: brush: QBrush) -> Optional[QPolygonF]:

View File

@@ -28,7 +28,7 @@ class QRecruitBehaviour:
bought_amount_labels = None bought_amount_labels = None
maximum_units = -1 maximum_units = -1
recruitable_types = [] recruitable_types = []
BUDGET_FORMAT = "Available Budget: <b>${}M</b>" BUDGET_FORMAT = "Available Budget: <b>${:.2f}M</b>"
def __init__(self) -> None: def __init__(self) -> None:
self.bought_amount_labels = {} self.bought_amount_labels = {}

View File

@@ -11,6 +11,8 @@ from game.theater import ControlPoint, TheaterGroundObject
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.uiconstants import VEHICLES_ICONS from qt_ui.uiconstants import VEHICLES_ICONS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from game import db
from dcs import vehicles
class QBaseDefenseGroupInfo(QGroupBox): class QBaseDefenseGroupInfo(QGroupBox):
@@ -71,7 +73,11 @@ class QBaseDefenseGroupInfo(QGroupBox):
icon.setText("<b>" + k[:8] + "</b>") icon.setText("<b>" + k[:8] + "</b>")
icon.setProperty("style", "icon-armor") icon.setProperty("style", "icon-armor")
self.unit_layout.addWidget(icon, i, 0) self.unit_layout.addWidget(icon, i, 0)
self.unit_layout.addWidget(QLabel(str(v) + " x " + "<strong>" + k + "</strong>"), i, 1) unit_display_name = k
unit_type = vehicles.vehicle_map.get(k)
if unit_type is not None:
unit_display_name = db.unit_get_expanded_info(self.game.enemy_country, unit_type, 'name')
self.unit_layout.addWidget(QLabel(str(v) + " x " + "<strong>" + unit_display_name + "</strong>"), i, 1)
i = i + 1 i = i + 1
if len(unit_dict.items()) == 0: if len(unit_dict.items()) == 0:

View File

@@ -29,6 +29,7 @@ from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QBudgetBox import QBudgetBox from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo
from dcs import vehicles
class QGroundObjectMenu(QDialog): class QGroundObjectMenu(QDialog):
@@ -101,7 +102,11 @@ class QGroundObjectMenu(QDialog):
if not hasattr(g, "units_losts"): if not hasattr(g, "units_losts"):
g.units_losts = [] g.units_losts = []
for u in g.units: for u in g.units:
self.intelLayout.addWidget(QLabel("<b>Unit #" + str(u.id) + " - " + str(u.type) + "</b>"), i, 0) unit_display_name = u.type
unit_type = vehicles.vehicle_map.get(u.type)
if unit_type is not None:
unit_display_name = db.unit_get_expanded_info(self.game.enemy_country, unit_type, 'name')
self.intelLayout.addWidget(QLabel("<b>Unit #" + str(u.id) + " - " + str(unit_display_name) + "</b>"), i, 0)
i = i + 1 i = i + 1
for u in g.units_losts: for u in g.units_losts:

View File

@@ -13,6 +13,7 @@ local unitPayloads = {
[1] = 30, [1] = 30,
}, },
}, },
},
["unitType"] = "SH-60B", ["unitType"] = "SH-60B",
} }
return unitPayloads return unitPayloads

View File

@@ -962,7 +962,7 @@
}], }],
"AAA Bofors 40mm": [{ "AAA Bofors 40mm": [{
"default": { "default": {
"name": "Bofors 40 mm gun", "name": "Bofors 40 mm Gun",
"country-of-origin": "Sweden", "country-of-origin": "Sweden",
"manufacturer": "Bofors", "manufacturer": "Bofors",
"role": "Anti-Aircraft Gun", "role": "Anti-Aircraft Gun",
@@ -1101,7 +1101,7 @@
}], }],
"APC M2A1": [{ "APC M2A1": [{
"default": { "default": {
"name": "M2A1 Half-track", "name": "M2A1 Half-Track",
"country-of-origin": "USA", "country-of-origin": "USA",
"manufacturer": "White Motor Company", "manufacturer": "White Motor Company",
"role": "Armoured Personnel Carrier", "role": "Armoured Personnel Carrier",