mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Add threat zone modeling.
Creates threat zones around airfields and non-trivial air defenses (it's not worth dodging anything with a threat range under 3nm). These threat zones can be used to aid mission planning and waypoint placement. https://github.com/Khopa/dcs_liberation/issues/292
This commit is contained in:
parent
e46262b021
commit
86558bdef6
35
game/game.py
35
game/game.py
@ -4,7 +4,7 @@ import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
@ -31,6 +31,7 @@ from .infos.information import Information
|
||||
from .procurement import ProcurementAi
|
||||
from .settings import Settings
|
||||
from .theater import ConflictTheater, ControlPoint
|
||||
from .threatzones import ThreatZones
|
||||
from .unitmap import UnitMap
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
@ -113,6 +114,9 @@ class Game:
|
||||
|
||||
self.sanitize_sides()
|
||||
|
||||
self.blue_threat_zone: ThreatZones
|
||||
self.red_threat_zone: ThreatZones
|
||||
|
||||
self.on_load()
|
||||
|
||||
# Turn 0 procurement. We don't actually have any missions to plan, but
|
||||
@ -126,6 +130,19 @@ class Game:
|
||||
|
||||
self.plan_procurement(blue_planner, red_planner)
|
||||
|
||||
def __getstate__(self) -> Dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting any volatile types that can be deterministically
|
||||
# recomputed on load for the sake of save compatibility.
|
||||
del state["blue_threat_zone"]
|
||||
del state["red_threat_zone"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.on_load()
|
||||
|
||||
def generate_conditions(self) -> Conditions:
|
||||
return Conditions.generate(self.theater, self.date,
|
||||
self.current_turn_time_of_day, self.settings)
|
||||
@ -151,6 +168,11 @@ class Game:
|
||||
def enemy_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.enemy_name]
|
||||
|
||||
def faction_for(self, player: bool) -> Faction:
|
||||
if player:
|
||||
return self.player_faction
|
||||
return self.enemy_faction
|
||||
|
||||
def _roll(self, prob, mult):
|
||||
if self.settings.version == "dev":
|
||||
# always generate all events for dev
|
||||
@ -227,6 +249,7 @@ class Game:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_conflicts_position()
|
||||
self.compute_threat_zones()
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
@ -290,6 +313,7 @@ class Game:
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.compute_conflicts_position()
|
||||
self.compute_threat_zones()
|
||||
self.ground_planners = {}
|
||||
self.blue_ato.clear()
|
||||
self.red_ato.clear()
|
||||
@ -353,6 +377,15 @@ class Game:
|
||||
self.current_group_id += 1
|
||||
return self.current_group_id
|
||||
|
||||
def compute_threat_zones(self) -> None:
|
||||
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
|
||||
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
|
||||
|
||||
def threat_zone_for(self, player: bool) -> ThreatZones:
|
||||
if player:
|
||||
return self.blue_threat_zone
|
||||
return self.red_threat_zone
|
||||
|
||||
def compute_conflicts_position(self):
|
||||
"""
|
||||
Compute the current conflict center position(s), mainly used for culling calculation
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Iterator, List, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
@ -9,6 +10,7 @@ from dcs.unitgroup import Group
|
||||
|
||||
from .. import db
|
||||
from ..data.radar_db import UNITS_WITH_RADAR
|
||||
from ..utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .controlpoint import ControlPoint
|
||||
@ -156,6 +158,23 @@ class TheaterGroundObject(MissionTarget):
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def threat_range(self) -> Distance:
|
||||
threat_range = meters(0)
|
||||
for group in self.groups:
|
||||
for u in group.units:
|
||||
unit = db.unit_type_from_name(u.type)
|
||||
if unit is None:
|
||||
logging.error(f"Unknown unit type {u.type}")
|
||||
continue
|
||||
|
||||
# Some units in pydcs have threat_range defined, but explicitly
|
||||
# set to None.
|
||||
unit_threat_range = getattr(unit, "threat_range", None)
|
||||
if unit_threat_range is not None:
|
||||
threat_range = max(threat_range, meters(unit_threat_range))
|
||||
return threat_range
|
||||
|
||||
|
||||
class BuildingGroundObject(TheaterGroundObject):
|
||||
def __init__(self, name: str, category: str, group_id: int, object_id: int,
|
||||
|
||||
92
game/threatzones.py
Normal file
92
game/threatzones.py
Normal file
@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import singledispatchmethod
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
from dcs.mapping import Point as DcsPoint
|
||||
from shapely.geometry import (
|
||||
LineString,
|
||||
MultiPolygon,
|
||||
Point as ShapelyPoint,
|
||||
Polygon,
|
||||
)
|
||||
from shapely.geometry.base import BaseGeometry
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from game.utils import nautical_miles
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
ThreatPoly = Union[MultiPolygon, Polygon]
|
||||
|
||||
|
||||
class ThreatZones:
|
||||
def __init__(self, airbases: ThreatPoly, air_defenses: ThreatPoly) -> None:
|
||||
self.airbases = airbases
|
||||
self.air_defenses = air_defenses
|
||||
self.all = unary_union([airbases, air_defenses])
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened_by_aircraft(self, target) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_aircraft.register
|
||||
def _threatened_by_aircraft_geom(self, position: BaseGeometry) -> bool:
|
||||
return self.airbases.intersects(position)
|
||||
|
||||
@threatened_by_aircraft.register
|
||||
def _threatened_by_aircraft_flight(self, flight: Flight) -> bool:
|
||||
return self.threatened_by_aircraft(LineString((
|
||||
self.dcs_to_shapely_point(p.position) for p in flight.points
|
||||
)))
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened_by_air_defense(self, target) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_air_defense.register
|
||||
def _threatened_by_air_defense_geom(self, position: BaseGeometry) -> bool:
|
||||
return self.air_defenses.intersects(position)
|
||||
|
||||
@threatened_by_air_defense.register
|
||||
def _threatened_by_air_defense_flight(self, flight: Flight) -> bool:
|
||||
return self.threatened_by_air_defense(LineString((
|
||||
self.dcs_to_shapely_point(p.position) for p in flight.points
|
||||
)))
|
||||
|
||||
@classmethod
|
||||
def for_faction(cls, game: Game, player: bool) -> ThreatZones:
|
||||
opposing_doctrine = game.faction_for(not player).doctrine
|
||||
|
||||
airbases = []
|
||||
air_defenses = []
|
||||
for control_point in game.theater.controlpoints:
|
||||
if control_point.captured != player:
|
||||
continue
|
||||
if control_point.runway_is_operational():
|
||||
point = ShapelyPoint(control_point.position.x,
|
||||
control_point.position.y)
|
||||
cap_threat_range = (opposing_doctrine.cap_max_distance_from_cp +
|
||||
opposing_doctrine.cap_engagement_range)
|
||||
airbases.append(point.buffer(cap_threat_range.meters))
|
||||
|
||||
for tgo in control_point.ground_objects:
|
||||
threat_range = tgo.threat_range
|
||||
# Any system with a shorter range than this is not worth even
|
||||
# avoiding.
|
||||
if threat_range > nautical_miles(3):
|
||||
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
||||
threat_zone = point.buffer(threat_range.meters)
|
||||
air_defenses.append(threat_zone)
|
||||
|
||||
return cls(
|
||||
airbases=unary_union(airbases),
|
||||
air_defenses=unary_union(air_defenses)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint:
|
||||
return ShapelyPoint(point.x, point.y)
|
||||
@ -59,6 +59,8 @@ class DisplayOptions:
|
||||
culling = DisplayRule("Display Culling Zones", False)
|
||||
flight_paths = FlightPathOptions()
|
||||
actual_frontline_pos = DisplayRule("Display Actual Frontline Location", False)
|
||||
blue_threat_zone = DisplayRule("Display Blue Threat Zones", False)
|
||||
red_threat_zone = DisplayRule("Display Red Threat Zones", False)
|
||||
|
||||
@classmethod
|
||||
def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]:
|
||||
|
||||
@ -26,6 +26,10 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
from dcs import Point
|
||||
from dcs.mapping import point_from_heading
|
||||
from shapely.geometry import (
|
||||
MultiPolygon,
|
||||
Polygon,
|
||||
)
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game, db
|
||||
@ -275,6 +279,35 @@ class QLiberationMap(QGraphicsView):
|
||||
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"])
|
||||
|
||||
def draw_shapely_poly(self, scene: QGraphicsScene, poly: Polygon, pen: QPen,
|
||||
brush: QBrush) -> None:
|
||||
if poly.is_empty:
|
||||
return
|
||||
points = []
|
||||
for x, y in poly.exterior.coords:
|
||||
x, y = self._transform_point(Point(x, y))
|
||||
points.append(QPointF(x, y))
|
||||
scene.addPolygon(QPolygonF(points), pen, brush)
|
||||
|
||||
def draw_threat_zone(self, scene: QGraphicsScene, poly: Polygon,
|
||||
player: bool) -> None:
|
||||
if player:
|
||||
brush = QColor(0, 132, 255, 100)
|
||||
else:
|
||||
brush = QColor(227, 32, 0, 100)
|
||||
self.draw_shapely_poly(scene, poly, CONST.COLORS["transparent"], brush)
|
||||
|
||||
def display_threat_zones(self, scene: QGraphicsScene,
|
||||
player: bool) -> None:
|
||||
"""Draws the threat zones on the map."""
|
||||
threat_poly = self.game.threat_zone_for(player).all
|
||||
if isinstance(threat_poly, MultiPolygon):
|
||||
polys = threat_poly.geoms
|
||||
else:
|
||||
polys = [threat_poly]
|
||||
for poly in polys:
|
||||
self.draw_threat_zone(scene, poly, player)
|
||||
|
||||
@staticmethod
|
||||
def should_display_ground_objects_at(cp: ControlPoint) -> bool:
|
||||
return ((DisplayOptions.sam_ranges and cp.captured) or
|
||||
@ -331,6 +364,12 @@ class QLiberationMap(QGraphicsView):
|
||||
if DisplayOptions.culling and self.game.settings.perf_culling:
|
||||
self.display_culling(scene)
|
||||
|
||||
if DisplayOptions.blue_threat_zone:
|
||||
self.display_threat_zones(scene, player=True)
|
||||
|
||||
if DisplayOptions.red_threat_zone:
|
||||
self.display_threat_zones(scene, player=False)
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
|
||||
pos = self._transform_point(cp.position)
|
||||
|
||||
@ -257,8 +257,6 @@ class QLiberationWindow(QMainWindow):
|
||||
|
||||
def setGame(self, game: Optional[Game]):
|
||||
try:
|
||||
if game is not None:
|
||||
game.on_load()
|
||||
self.game = game
|
||||
if self.info_panel is not None:
|
||||
self.info_panel.setGame(game)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user