mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge 'upstream/develop' into new-plugin-system
This commit is contained in:
commit
b1840ce2ca
15
changelog.md
15
changelog.md
@ -1,3 +1,18 @@
|
||||
# 2.2.X
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Flight Planner]** Flight planner overhaul, with package and TOT system
|
||||
* **[Map]** Highlight the selected flight path on the map
|
||||
* **[Map]** Improved flight plan display settings
|
||||
* **[Map]** Improved SAM display settings
|
||||
* **[Map]** Added polygon debug mode display
|
||||
* **[New Game]** Starting budget can be freely selected
|
||||
* **[Moddability]** Custom campaigns can be designed through json files
|
||||
|
||||
## Fixes :
|
||||
* **[Campaign generator]** Ship group and offshore buildings should not be generated on land anymore
|
||||
|
||||
|
||||
# 2.1.5
|
||||
|
||||
## Features/Improvements :
|
||||
|
||||
@ -12,7 +12,6 @@ from game import db, persistency
|
||||
from game.debriefing import Debriefing
|
||||
from game.infos.information import Information
|
||||
from game.operation.operation import Operation
|
||||
from gen.environmentgen import EnvironmentSettings
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from theater import ControlPoint
|
||||
from theater.start_generator import generate_airbase_defense_group
|
||||
@ -42,7 +41,6 @@ class Event:
|
||||
|
||||
operation = None # type: Operation
|
||||
difficulty = 1 # type: int
|
||||
environment_settings = None # type: EnvironmentSettings
|
||||
BONUS_BASE = 5
|
||||
|
||||
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
|
||||
|
||||
24
game/game.py
24
game/game.py
@ -2,7 +2,7 @@ import logging
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from dcs.action import Coalition
|
||||
@ -29,6 +29,7 @@ from .event.frontlineattack import FrontlineAttackEvent
|
||||
from .infos.information import Information
|
||||
from .settings import Settings
|
||||
from plugin import LuaPluginManager
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
COMMISION_UNIT_VARIETY = 4
|
||||
COMMISION_LIMITS_SCALE = 1.5
|
||||
@ -79,7 +80,7 @@ class Game:
|
||||
self.enemy_name = enemy_name
|
||||
self.enemy_country = db.FACTIONS[enemy_name]["country"]
|
||||
self.turn = 0
|
||||
self.date = datetime(start_date.year, start_date.month, start_date.day)
|
||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||
self.game_stats = GameStats()
|
||||
self.game_stats.update(self)
|
||||
self.ground_planners: Dict[int, GroundPlanner] = {}
|
||||
@ -92,6 +93,8 @@ class Game:
|
||||
self.current_unit_id = 0
|
||||
self.current_group_id = 0
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
self.blue_ato = AirTaskingOrder()
|
||||
self.red_ato = AirTaskingOrder()
|
||||
|
||||
@ -102,6 +105,9 @@ class Game:
|
||||
self.sanitize_sides()
|
||||
self.on_load()
|
||||
|
||||
def generate_conditions(self) -> Conditions:
|
||||
return Conditions.generate(self.theater, self.date,
|
||||
self.current_turn_time_of_day, self.settings)
|
||||
|
||||
def sanitize_sides(self):
|
||||
"""
|
||||
@ -223,6 +229,12 @@ class Game:
|
||||
for plugin in LuaPluginManager().getPlugins():
|
||||
plugin.setSettings(self.settings)
|
||||
|
||||
# Save game compatibility.
|
||||
|
||||
# TODO: Remove in 2.3.
|
||||
if not hasattr(self, "conditions"):
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
def pass_turn(self, no_action=False):
|
||||
logging.info("Pass turn")
|
||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
||||
@ -257,6 +269,8 @@ class Game:
|
||||
for cp in self.theater.controlpoints:
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.__culling_points = self.compute_conflicts_position()
|
||||
self.ground_planners = {}
|
||||
@ -345,11 +359,11 @@ class Game:
|
||||
self.informations.append(info)
|
||||
|
||||
@property
|
||||
def current_turn_daytime(self):
|
||||
return ["dawn", "day", "dusk", "night"][self.turn % 4]
|
||||
def current_turn_time_of_day(self) -> TimeOfDay:
|
||||
return list(TimeOfDay)[self.turn % 4]
|
||||
|
||||
@property
|
||||
def current_day(self):
|
||||
def current_day(self) -> date:
|
||||
return self.date + timedelta(days=self.turn // 4)
|
||||
|
||||
def next_unit_id(self):
|
||||
|
||||
@ -65,16 +65,6 @@ class ControlPointAircraftInventory:
|
||||
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()
|
||||
|
||||
@ -21,7 +21,7 @@ from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
|
||||
from gen.armor import GroundConflictGenerator, JtacInfo
|
||||
from gen.beacons import load_beacons_for_terrain
|
||||
from gen.briefinggen import BriefingGenerator
|
||||
from gen.environmentgen import EnviromentGenerator
|
||||
from gen.environmentgen import EnvironmentGenerator
|
||||
from gen.forcedoptionsgen import ForcedOptionsGenerator
|
||||
from gen.groundobjectsgen import GroundObjectsGenerator
|
||||
from gen.kneeboard import KneeboardGenerator
|
||||
@ -45,7 +45,6 @@ class Operation:
|
||||
triggersgen = None # type: TriggersGenerator
|
||||
airsupportgen = None # type: AirSupportConflictGenerator
|
||||
visualgen = None # type: VisualGenerator
|
||||
envgen = None # type: EnviromentGenerator
|
||||
groundobjectgen = None # type: GroundObjectsGenerator
|
||||
briefinggen = None # type: BriefingGenerator
|
||||
forcedoptionsgen = None # type: ForcedOptionsGenerator
|
||||
@ -191,13 +190,9 @@ class Operation:
|
||||
for frequency in unique_map_frequencies:
|
||||
radio_registry.reserve(frequency)
|
||||
|
||||
# Generate meteo
|
||||
envgen = EnviromentGenerator(self.current_mission, self.conflict,
|
||||
self.game)
|
||||
if self.environment_settings is None:
|
||||
self.environment_settings = envgen.generate()
|
||||
else:
|
||||
envgen.load(self.environment_settings)
|
||||
# Set mission time and weather conditions.
|
||||
EnvironmentGenerator(self.current_mission,
|
||||
self.game.conditions).generate()
|
||||
|
||||
# Generate ground object first
|
||||
|
||||
|
||||
@ -45,4 +45,15 @@ class Settings:
|
||||
plugin.setSettings(self)
|
||||
|
||||
|
||||
# Cheating
|
||||
self.show_red_ato = False
|
||||
|
||||
def __setstate__(self, state) -> None:
|
||||
# __setstate__ is called with the dict of the object being unpickled. We
|
||||
# can provide save compatibility for new settings options (which
|
||||
# normally would not be present in the unpickled object) by creating a
|
||||
# new settings object, updating it with the unpickled state, and
|
||||
# updating our dict with that.
|
||||
new_state = Settings().__dict__
|
||||
new_state.update(state)
|
||||
self.__dict__.update(new_state)
|
||||
|
||||
183
game/weather.py
Normal file
183
game/weather.py
Normal file
@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from dcs.weather import Weather as PydcsWeather, Wind
|
||||
|
||||
from game.settings import Settings
|
||||
from theater import ConflictTheater
|
||||
|
||||
|
||||
class TimeOfDay(Enum):
|
||||
Dawn = "dawn"
|
||||
Day = "day"
|
||||
Dusk = "dusk"
|
||||
Night = "night"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindConditions:
|
||||
at_0m: Wind
|
||||
at_2000m: Wind
|
||||
at_8000m: Wind
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Clouds:
|
||||
base: int
|
||||
density: int
|
||||
thickness: int
|
||||
precipitation: PydcsWeather.Preceptions
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Fog:
|
||||
visibility: int
|
||||
thickness: int
|
||||
|
||||
|
||||
class Weather:
|
||||
def __init__(self) -> None:
|
||||
self.clouds = self.generate_clouds()
|
||||
self.fog = self.generate_fog()
|
||||
self.wind = self.generate_wind()
|
||||
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
raise NotImplementedError
|
||||
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
if random.randrange(5) != 0:
|
||||
return None
|
||||
return Fog(
|
||||
visibility=random.randint(2500, 5000),
|
||||
thickness=random.randint(100, 500)
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def random_wind(minimum: int, maximum) -> WindConditions:
|
||||
wind_direction = random.randint(0, 360)
|
||||
at_0m_factor = 1
|
||||
at_2000m_factor = 2
|
||||
at_8000m_factor = 3
|
||||
base_wind = random.randint(minimum, maximum)
|
||||
|
||||
return WindConditions(
|
||||
# Always some wind to make the smoke move a bit.
|
||||
at_0m=Wind(wind_direction, min(1, base_wind * at_0m_factor)),
|
||||
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
|
||||
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_base() -> int:
|
||||
return random.randint(2000, 3000)
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_thickness() -> int:
|
||||
return random.randint(100, 400)
|
||||
|
||||
|
||||
class ClearSkies(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return None
|
||||
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 0)
|
||||
|
||||
|
||||
class Cloudy(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(1, 8),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.None_
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 4)
|
||||
|
||||
|
||||
class Raining(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(5, 8),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.Rain
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 6)
|
||||
|
||||
|
||||
class Thunderstorm(Weather):
|
||||
def generate_clouds(self) -> Optional[Clouds]:
|
||||
return Clouds(
|
||||
base=self.random_cloud_base(),
|
||||
density=random.randint(9, 10),
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.Thunderstorm
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(0, 8)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Conditions:
|
||||
time_of_day: TimeOfDay
|
||||
start_time: datetime.datetime
|
||||
weather: Weather
|
||||
|
||||
@classmethod
|
||||
def generate(cls, theater: ConflictTheater, day: datetime.date,
|
||||
time_of_day: TimeOfDay, settings: Settings) -> Conditions:
|
||||
return cls(
|
||||
time_of_day=time_of_day,
|
||||
start_time=cls.generate_start_time(
|
||||
theater, day, time_of_day, settings.night_disabled
|
||||
),
|
||||
weather=cls.generate_weather()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate_start_time(cls, theater: ConflictTheater, day: datetime.date,
|
||||
time_of_day: TimeOfDay,
|
||||
night_disabled: bool) -> datetime.datetime:
|
||||
if night_disabled:
|
||||
logging.info("Skip Night mission due to user settings")
|
||||
time_range = {
|
||||
TimeOfDay.Dawn: (8, 9),
|
||||
TimeOfDay.Day: (10, 12),
|
||||
TimeOfDay.Dusk: (12, 14),
|
||||
TimeOfDay.Night: (14, 17),
|
||||
}[time_of_day]
|
||||
else:
|
||||
time_range = theater.daytime_map[time_of_day.value]
|
||||
|
||||
time = datetime.time(hour=random.randint(*time_range))
|
||||
return datetime.datetime.combine(day, time)
|
||||
|
||||
@classmethod
|
||||
def generate_weather(cls) -> Weather:
|
||||
chances = {
|
||||
Thunderstorm: 1,
|
||||
Raining: 20,
|
||||
Cloudy: 60,
|
||||
ClearSkies: 20,
|
||||
}
|
||||
weather_type = random.choices(list(chances.keys()),
|
||||
weights=list(chances.values()))[0]
|
||||
return weather_type()
|
||||
@ -419,6 +419,7 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator):
|
||||
|
||||
# TODO : Some GCI on Channel 4 ?
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftData:
|
||||
"""Additional aircraft data not exposed by pydcs."""
|
||||
@ -442,7 +443,9 @@ class AircraftData:
|
||||
AIRCRAFT_DATA: Dict[str, AircraftData] = {
|
||||
"A-10C": AircraftData(
|
||||
inter_flight_radio=get_radio("AN/ARC-164"),
|
||||
intra_flight_radio=get_radio("AN/ARC-164"), # VHF for intraflight is not accepted anymore by DCS (see https://forums.eagle.ru/showthread.php?p=4499738)
|
||||
# VHF for intraflight is not accepted anymore by DCS
|
||||
# (see https://forums.eagle.ru/showthread.php?p=4499738).
|
||||
intra_flight_radio=get_radio("AN/ARC-164"),
|
||||
channel_allocator=WarthogRadioChannelAllocator()
|
||||
),
|
||||
|
||||
@ -532,6 +535,7 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
|
||||
channel_namer=SCR522ChannelNamer
|
||||
),
|
||||
}
|
||||
AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
|
||||
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
|
||||
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
|
||||
|
||||
@ -793,6 +797,8 @@ class AircraftConflictGenerator:
|
||||
self.clear_parking_slots()
|
||||
|
||||
for package in ato.packages:
|
||||
if not package.flights:
|
||||
continue
|
||||
timing = PackageWaypointTiming.for_package(package)
|
||||
for flight in package.flights:
|
||||
culled = self.game.position_culled(flight.from_cp.position)
|
||||
@ -996,7 +1002,7 @@ class AircraftConflictGenerator:
|
||||
flight: Flight, timing: PackageWaypointTiming,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
flight_type = flight.flight_type
|
||||
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP,
|
||||
if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
|
||||
FlightType.INTERCEPTION]:
|
||||
self.configure_cap(group, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.CAS, FlightType.BAI]:
|
||||
@ -1135,7 +1141,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
pattern=OrbitAction.OrbitPattern.Circle
|
||||
))
|
||||
loiter.stop_after_time(
|
||||
self.timing.push_time(self.flight, waypoint.position))
|
||||
self.timing.push_time(self.flight, self.waypoint))
|
||||
waypoint.add_task(loiter)
|
||||
return waypoint
|
||||
|
||||
|
||||
@ -129,6 +129,11 @@ class BriefingGenerator(MissionInfoGenerator):
|
||||
self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n"
|
||||
self.description += "=" * 15 + "\n\n"
|
||||
|
||||
self.description += (
|
||||
"Most briefing information, including communications and flight "
|
||||
"plan information, can be found on your kneeboard.\n\n"
|
||||
)
|
||||
|
||||
self.generate_ongoing_war_text()
|
||||
|
||||
self.description += "\n"*2
|
||||
|
||||
@ -1,147 +1,36 @@
|
||||
import logging
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from dcs.mission import Mission
|
||||
from dcs.weather import Weather, Wind
|
||||
|
||||
from .conflictgen import Conflict
|
||||
|
||||
WEATHER_CLOUD_BASE = 2000, 3000
|
||||
WEATHER_CLOUD_DENSITY = 1, 8
|
||||
WEATHER_CLOUD_THICKNESS = 100, 400
|
||||
WEATHER_CLOUD_BASE_MIN = 1600
|
||||
|
||||
WEATHER_FOG_CHANCE = 20
|
||||
WEATHER_FOG_VISIBILITY = 2500, 5000
|
||||
WEATHER_FOG_THICKNESS = 100, 500
|
||||
|
||||
RANDOM_TIME = {
|
||||
"night": 7,
|
||||
"dusk": 40,
|
||||
"dawn": 40,
|
||||
"day": 100,
|
||||
}
|
||||
|
||||
RANDOM_WEATHER = {
|
||||
1: 0, # thunderstorm
|
||||
2: 20, # rain
|
||||
3: 80, # clouds
|
||||
4: 100, # clear
|
||||
}
|
||||
from game.weather import Clouds, Fog, Conditions, WindConditions
|
||||
|
||||
|
||||
class EnvironmentSettings:
|
||||
weather_dict = None
|
||||
start_time = None
|
||||
|
||||
|
||||
class EnviromentGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game):
|
||||
class EnvironmentGenerator:
|
||||
def __init__(self, mission: Mission, conditions: Conditions) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.conditions = conditions
|
||||
|
||||
def _gen_time(self):
|
||||
def set_clouds(self, clouds: Optional[Clouds]) -> None:
|
||||
if clouds is None:
|
||||
return
|
||||
self.mission.weather.clouds_base = clouds.base
|
||||
self.mission.weather.clouds_thickness = clouds.thickness
|
||||
self.mission.weather.clouds_density = clouds.density
|
||||
self.mission.weather.clouds_iprecptns = clouds.precipitation
|
||||
|
||||
start_time = self.game.current_day
|
||||
def set_fog(self, fog: Optional[Fog]) -> None:
|
||||
if fog is None:
|
||||
return
|
||||
self.mission.weather.fog_visibility = fog.visibility
|
||||
self.mission.weather.fog_thickness = fog.thickness
|
||||
|
||||
daytime = self.game.current_turn_daytime
|
||||
logging.info("Mission time will be {}".format(daytime))
|
||||
if self.game.settings.night_disabled:
|
||||
logging.info("Skip Night mission due to user settings")
|
||||
if daytime == "dawn":
|
||||
time_range = (8, 9)
|
||||
elif daytime == "day":
|
||||
time_range = (10, 12)
|
||||
elif daytime == "dusk":
|
||||
time_range = (12, 14)
|
||||
elif daytime == "night":
|
||||
time_range = (14, 17)
|
||||
else:
|
||||
time_range = (10, 12)
|
||||
else:
|
||||
time_range = self.game.theater.daytime_map[daytime]
|
||||
|
||||
start_time += timedelta(hours=random.randint(*time_range))
|
||||
|
||||
logging.info("time - {}, slot - {}, night skipped - {}".format(
|
||||
str(start_time),
|
||||
str(time_range),
|
||||
self.game.settings.night_disabled))
|
||||
|
||||
self.mission.start_time = start_time
|
||||
|
||||
def _generate_wind(self, wind_speed, wind_direction=None):
|
||||
# wind
|
||||
if not wind_direction:
|
||||
wind_direction = random.randint(0, 360)
|
||||
|
||||
self.mission.weather.wind_at_ground = Wind(wind_direction, wind_speed)
|
||||
self.mission.weather.wind_at_2000 = Wind(wind_direction, wind_speed * 2)
|
||||
self.mission.weather.wind_at_8000 = Wind(wind_direction, wind_speed * 3)
|
||||
|
||||
def _generate_base_weather(self):
|
||||
# clouds
|
||||
self.mission.weather.clouds_base = random.randint(*WEATHER_CLOUD_BASE)
|
||||
self.mission.weather.clouds_density = random.randint(*WEATHER_CLOUD_DENSITY)
|
||||
self.mission.weather.clouds_thickness = random.randint(*WEATHER_CLOUD_THICKNESS)
|
||||
|
||||
# wind
|
||||
self._generate_wind(random.randint(0, 4))
|
||||
|
||||
# fog
|
||||
if random.randint(0, 100) < WEATHER_FOG_CHANCE:
|
||||
self.mission.weather.fog_visibility = random.randint(*WEATHER_FOG_VISIBILITY)
|
||||
self.mission.weather.fog_thickness = random.randint(*WEATHER_FOG_THICKNESS)
|
||||
|
||||
def _gen_random_weather(self):
|
||||
weather_type = None
|
||||
for k, v in RANDOM_WEATHER.items():
|
||||
if random.randint(0, 100) <= v:
|
||||
weather_type = k
|
||||
break
|
||||
|
||||
logging.info("generated weather {}".format(weather_type))
|
||||
if weather_type == 1:
|
||||
# thunderstorm
|
||||
self._generate_base_weather()
|
||||
self._generate_wind(random.randint(0, 8))
|
||||
|
||||
self.mission.weather.clouds_density = random.randint(9, 10)
|
||||
self.mission.weather.clouds_iprecptns = Weather.Preceptions.Thunderstorm
|
||||
elif weather_type == 2:
|
||||
# rain
|
||||
self._generate_base_weather()
|
||||
self.mission.weather.clouds_density = random.randint(5, 8)
|
||||
self.mission.weather.clouds_iprecptns = Weather.Preceptions.Rain
|
||||
|
||||
self._generate_wind(random.randint(0, 6))
|
||||
elif weather_type == 3:
|
||||
# clouds
|
||||
self._generate_base_weather()
|
||||
elif weather_type == 4:
|
||||
# clear
|
||||
pass
|
||||
|
||||
if self.mission.weather.clouds_density > 0:
|
||||
# sometimes clouds are randomized way too low and need to be fixed
|
||||
self.mission.weather.clouds_base = max(self.mission.weather.clouds_base, WEATHER_CLOUD_BASE_MIN)
|
||||
|
||||
if self.mission.weather.wind_at_ground.speed == 0:
|
||||
# frontline smokes look silly w/o any wind
|
||||
self._generate_wind(1)
|
||||
|
||||
def generate(self) -> EnvironmentSettings:
|
||||
self._gen_time()
|
||||
self._gen_random_weather()
|
||||
|
||||
settings = EnvironmentSettings()
|
||||
settings.start_time = self.mission.start_time
|
||||
settings.weather_dict = self.mission.weather.dict()
|
||||
return settings
|
||||
|
||||
def load(self, settings: EnvironmentSettings):
|
||||
self.mission.start_time = settings.start_time
|
||||
self.mission.weather.load_from_dict(settings.weather_dict)
|
||||
def set_wind(self, wind: WindConditions) -> None:
|
||||
self.mission.weather.wind_at_ground = wind.at_0m
|
||||
self.mission.weather.wind_at_2000 = wind.at_2000m
|
||||
self.mission.weather.wind_at_8000 = wind.at_8000m
|
||||
|
||||
def generate(self):
|
||||
self.mission.start_time = self.conditions.start_time
|
||||
self.set_clouds(self.conditions.weather.clouds)
|
||||
self.set_fog(self.conditions.weather.fog)
|
||||
self.set_wind(self.conditions.weather.wind)
|
||||
|
||||
@ -131,7 +131,7 @@ class AircraftAllocator:
|
||||
|
||||
@staticmethod
|
||||
def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP)
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_PREFERRED
|
||||
elif task == FlightType.CAS:
|
||||
@ -147,7 +147,7 @@ class AircraftAllocator:
|
||||
|
||||
@staticmethod
|
||||
def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP)
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
@ -404,7 +404,7 @@ class CoalitionMissionPlanner:
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
yield ProposedMission(cp, [
|
||||
ProposedFlight(FlightType.CAP, 2, self.MAX_CAP_RANGE),
|
||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||
])
|
||||
|
||||
# Find front lines, plan CAP.
|
||||
@ -493,11 +493,7 @@ class CoalitionMissionPlanner:
|
||||
error = random.randint(-margin, margin)
|
||||
yield max(0, time + error)
|
||||
|
||||
dca_types = (
|
||||
FlightType.BARCAP,
|
||||
FlightType.CAP,
|
||||
FlightType.INTERCEPTION,
|
||||
)
|
||||
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION)
|
||||
|
||||
non_dca_packages = [p for p in self.ato.packages if
|
||||
p.primary_task not in dca_types]
|
||||
|
||||
@ -326,7 +326,7 @@ SEAD_CAPABLE = [
|
||||
F_4E,
|
||||
FA_18C_hornet,
|
||||
F_15E,
|
||||
# F_16C_50, Not yet
|
||||
F_16C_50,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from enum import Enum
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
from game import db
|
||||
from dcs.unittype import UnitType
|
||||
@ -8,7 +8,7 @@ from theater.controlpoint import ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
CAP = 0
|
||||
CAP = 0 # Do not use. Use BARCAP or TARCAP.
|
||||
TARCAP = 1
|
||||
BARCAP = 2
|
||||
CAS = 3
|
||||
@ -152,6 +152,14 @@ class Flight:
|
||||
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
||||
+ " (" + str(len(self.points)) + " wpt)"
|
||||
|
||||
def waypoint_with_type(
|
||||
self,
|
||||
types: Iterable[FlightWaypointType]) -> Optional[FlightWaypoint]:
|
||||
for waypoint in self.points:
|
||||
if waypoint.waypoint_type in types:
|
||||
return waypoint
|
||||
return None
|
||||
|
||||
|
||||
# Test
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -69,8 +69,6 @@ class FlightPlanBuilder:
|
||||
logging.error("BAI flight plan generation not implemented")
|
||||
elif task == FlightType.BARCAP:
|
||||
self.generate_barcap(flight)
|
||||
elif task == FlightType.CAP:
|
||||
self.generate_barcap(flight)
|
||||
elif task == FlightType.CAS:
|
||||
self.generate_cas(flight)
|
||||
elif task == FlightType.DEAD:
|
||||
@ -103,8 +101,10 @@ class FlightPlanBuilder:
|
||||
logging.error(
|
||||
"Troop transport flight plan generation not implemented"
|
||||
)
|
||||
except InvalidObjectiveLocation as ex:
|
||||
logging.error(f"Could not create flight plan: {ex}")
|
||||
else:
|
||||
logging.error(f"Unsupported task type: {task.name}")
|
||||
except InvalidObjectiveLocation:
|
||||
logging.exception(f"Could not create flight plan")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
ingress_point = self._ingress_point()
|
||||
@ -424,7 +424,6 @@ class FlightPlanBuilder:
|
||||
def _heading_to_package_airfield(self, point: Point) -> int:
|
||||
return self.package_airfield().position.heading_between_point(point)
|
||||
|
||||
# TODO: Set ingress/egress/join/split points in the Package.
|
||||
def package_airfield(self) -> ControlPoint:
|
||||
# We'll always have a package, but if this is being planned via the UI
|
||||
# it could be the first flight in the package.
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from gen.ato import Package
|
||||
@ -17,25 +19,101 @@ from gen.flights.flight import (
|
||||
|
||||
|
||||
CAP_DURATION = 30 # Minutes
|
||||
CAP_TYPES = (FlightType.BARCAP, FlightType.CAP)
|
||||
|
||||
INGRESS_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
|
||||
IP_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
}
|
||||
|
||||
|
||||
class GroundSpeed:
|
||||
@classmethod
|
||||
def for_package(cls, package: Package) -> int:
|
||||
speeds = []
|
||||
for flight in package.flights:
|
||||
speeds.append(cls.for_flight(flight))
|
||||
return min(speeds) # knots
|
||||
|
||||
@staticmethod
|
||||
def for_flight(_flight: Flight) -> int:
|
||||
# TODO: Gather data so this is useful.
|
||||
def mission_speed(package: Package) -> int:
|
||||
speeds = set()
|
||||
for flight in package.flights:
|
||||
waypoint = flight.waypoint_with_type(IP_TYPES)
|
||||
if waypoint is None:
|
||||
logging.error(f"Could not find ingress point for {flight}.")
|
||||
if flight.points:
|
||||
logging.warning(
|
||||
"Using first waypoint for mission altitude.")
|
||||
waypoint = flight.points[0]
|
||||
else:
|
||||
logging.warning(
|
||||
"Flight has no waypoints. Assuming mission altitude "
|
||||
"of 25000 feet.")
|
||||
waypoint = FlightWaypoint(FlightWaypointType.NAV, 0, 0,
|
||||
25000)
|
||||
speeds.add(GroundSpeed.for_flight(flight, waypoint.alt))
|
||||
return min(speeds)
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, altitude: int) -> int:
|
||||
if not issubclass(flight.unit_type, FlyingType):
|
||||
raise TypeError("Flight has non-flying unit")
|
||||
|
||||
# TODO: Expose both a cruise speed and target speed.
|
||||
# The cruise speed can be used for ascent, hold, join, and RTB to save
|
||||
# on fuel, but mission speed will be fast enough to keep the flight
|
||||
# safer.
|
||||
return 400 # knots
|
||||
|
||||
c_sound_sea_level = 661.5
|
||||
|
||||
# DCS's max speed is in kph at 0 MSL. Convert to knots.
|
||||
max_speed = flight.unit_type.max_speed * 0.539957
|
||||
if max_speed > c_sound_sea_level:
|
||||
# Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
|
||||
# account for heavily loaded jets.
|
||||
return int(cls.from_mach(0.8, altitude))
|
||||
|
||||
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
||||
# 80% of its maximum, and that it can maintain the same mach at altitude
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
mach = max_speed * 0.8 / c_sound_sea_level
|
||||
return int(cls.from_mach(mach, altitude)) # knots
|
||||
|
||||
@staticmethod
|
||||
def from_mach(mach: float, altitude: int) -> float:
|
||||
"""Returns the ground speed in knots for the given mach and altitude.
|
||||
|
||||
Args:
|
||||
mach: The mach number to convert to ground speed.
|
||||
altitude: The altitude in feet.
|
||||
|
||||
Returns:
|
||||
The ground speed corresponding to the given altitude and mach number
|
||||
in knots.
|
||||
"""
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||
if altitude <= 36152:
|
||||
temperature_f = 59 - 0.00356 * altitude
|
||||
else:
|
||||
# There's another formula for altitudes over 82k feet, but we better
|
||||
# not be planning waypoints that high...
|
||||
temperature_f = -70
|
||||
|
||||
temperature_k = (temperature_f + 459.67) * (5 / 9)
|
||||
|
||||
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
|
||||
# Dependent on temperature, but varies very little (+/-0.001)
|
||||
# between -40F and 180F.
|
||||
heat_capacity_ratio = 1.4
|
||||
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
|
||||
gas_constant = 286 # m^2/s^2/K
|
||||
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
|
||||
# c_sound is in m/s, convert to knots.
|
||||
return (c_sound * 1.944) * mach
|
||||
|
||||
|
||||
class TravelTime:
|
||||
@ -72,7 +150,7 @@ class TotEstimator:
|
||||
# Takeoff immediately.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
start_time = self.timing.race_track_start
|
||||
else:
|
||||
start_time = self.timing.join
|
||||
@ -97,13 +175,7 @@ class TotEstimator:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
"""
|
||||
stop_types = {
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
time_to_ingress = self.estimate_waypoints_to_target(flight, stop_types)
|
||||
time_to_ingress = self.estimate_waypoints_to_target(flight, IP_TYPES)
|
||||
if time_to_ingress is None:
|
||||
logging.warning(
|
||||
f"Found no ingress types. Cannot estimate TOT for {flight}")
|
||||
@ -111,7 +183,7 @@ class TotEstimator:
|
||||
# the package.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
# The racetrack start *is* the target. The package target is the
|
||||
# protected objective.
|
||||
time_to_target = 0
|
||||
@ -119,7 +191,7 @@ class TotEstimator:
|
||||
assert self.package.waypoints is not None
|
||||
time_to_target = TravelTime.between_points(
|
||||
self.package.waypoints.ingress, self.package.target.position,
|
||||
GroundSpeed.for_package(self.package))
|
||||
GroundSpeed.mission_speed(self.package))
|
||||
return sum([
|
||||
self.estimate_startup(flight),
|
||||
self.estimate_ground_ops(flight),
|
||||
@ -146,30 +218,38 @@ class TotEstimator:
|
||||
self, flight: Flight,
|
||||
stop_types: Iterable[FlightWaypointType]) -> Optional[int]:
|
||||
total = 0
|
||||
# TODO: This is AGL. We want MSL.
|
||||
previous_altitude = 0
|
||||
previous_position = flight.from_cp.position
|
||||
for waypoint in flight.points:
|
||||
position = Point(waypoint.x, waypoint.y)
|
||||
total += TravelTime.between_points(
|
||||
previous_position, position,
|
||||
self.speed_to_waypoint(flight, waypoint)
|
||||
self.speed_to_waypoint(flight, waypoint, previous_altitude)
|
||||
)
|
||||
previous_position = position
|
||||
previous_altitude = waypoint.alt
|
||||
if waypoint.waypoint_type in stop_types:
|
||||
return total
|
||||
|
||||
return None
|
||||
|
||||
def speed_to_waypoint(self, flight: Flight,
|
||||
waypoint: FlightWaypoint) -> int:
|
||||
def speed_to_waypoint(self, flight: Flight, waypoint: FlightWaypoint,
|
||||
from_altitude: int) -> int:
|
||||
# TODO: Adjust if AGL.
|
||||
# We don't have an exact heightmap, but we should probably be performing
|
||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||
# near 2000 ft MSL.
|
||||
alt_for_speed = min(from_altitude, waypoint.alt)
|
||||
pre_join = (FlightWaypointType.LOITER, FlightWaypointType.JOIN)
|
||||
if waypoint.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
||||
# Flights that start airborne already have some altitude and a good
|
||||
# amount of speed.
|
||||
factor = 1.0 if flight.start_type == "In Flight" else 0.5
|
||||
return int(GroundSpeed.for_flight(flight) * factor)
|
||||
return int(GroundSpeed.for_flight(flight, alt_for_speed) * factor)
|
||||
elif waypoint.waypoint_type in pre_join:
|
||||
return GroundSpeed.for_flight(flight)
|
||||
return GroundSpeed.for_package(self.package)
|
||||
return GroundSpeed.for_flight(flight, alt_for_speed)
|
||||
return GroundSpeed.mission_speed(self.package)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -197,24 +277,24 @@ class PackageWaypointTiming:
|
||||
|
||||
@property
|
||||
def race_track_start(self) -> int:
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
return self.package.time_over_target
|
||||
else:
|
||||
return self.ingress
|
||||
|
||||
@property
|
||||
def race_track_end(self) -> int:
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
return self.target + CAP_DURATION * 60
|
||||
else:
|
||||
return self.egress
|
||||
|
||||
def push_time(self, flight: Flight, hold_point: Point) -> int:
|
||||
def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int:
|
||||
assert self.package.waypoints is not None
|
||||
return self.join - TravelTime.between_points(
|
||||
hold_point,
|
||||
Point(hold_point.x, hold_point.y),
|
||||
self.package.waypoints.join,
|
||||
GroundSpeed.for_flight(flight)
|
||||
GroundSpeed.for_flight(flight, hold_point.alt)
|
||||
)
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]:
|
||||
@ -224,15 +304,9 @@ class PackageWaypointTiming:
|
||||
FlightWaypointType.TARGET_SHIP,
|
||||
)
|
||||
|
||||
ingress_types = (
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
)
|
||||
|
||||
if waypoint.waypoint_type == FlightWaypointType.JOIN:
|
||||
return self.join
|
||||
elif waypoint.waypoint_type in ingress_types:
|
||||
elif waypoint.waypoint_type in INGRESS_TYPES:
|
||||
return self.ingress
|
||||
elif waypoint.waypoint_type in target_types:
|
||||
return self.target
|
||||
@ -247,7 +321,7 @@ class PackageWaypointTiming:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
|
||||
flight: Flight) -> Optional[int]:
|
||||
if waypoint.waypoint_type == FlightWaypointType.LOITER:
|
||||
return self.push_time(flight, Point(waypoint.x, waypoint.y))
|
||||
return self.push_time(flight, waypoint)
|
||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
|
||||
return self.race_track_end
|
||||
return None
|
||||
@ -256,7 +330,17 @@ class PackageWaypointTiming:
|
||||
def for_package(cls, package: Package) -> PackageWaypointTiming:
|
||||
assert package.waypoints is not None
|
||||
|
||||
group_ground_speed = GroundSpeed.for_package(package)
|
||||
# TODO: Plan similar altitudes for the in-country leg of the mission.
|
||||
# Waypoint altitudes for a given flight *shouldn't* differ too much
|
||||
# between the join and split points, so we don't need speeds for each
|
||||
# leg individually since they should all be fairly similar. This doesn't
|
||||
# hold too well right now since nothing is stopping each waypoint from
|
||||
# jumping 20k feet each time, but that's a huge waste of energy we
|
||||
# should be avoiding anyway.
|
||||
if not package.flights:
|
||||
raise ValueError("Cannot plan TOT for package with no flights")
|
||||
|
||||
group_ground_speed = GroundSpeed.mission_speed(package)
|
||||
|
||||
ingress = package.time_over_target - TravelTime.between_points(
|
||||
package.waypoints.ingress,
|
||||
|
||||
@ -267,12 +267,6 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Race-track start"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
# TODO: Does this actually do anything?
|
||||
# orbit0.targets.append(location)
|
||||
# Note: Targets of PATROL TRACK waypoints are the points to be defended.
|
||||
# orbit0.targets.append(flight.from_cp)
|
||||
# orbit0.targets.append(center)
|
||||
|
||||
def race_track_end(self, position: Point, altitude: int) -> None:
|
||||
"""Creates a racetrack end waypoint.
|
||||
|
||||
|
||||
2
pydcs
2
pydcs
@ -1 +1 @@
|
||||
Subproject commit c203e5a1b8d5eb42d559dab074e668bf37fa5158
|
||||
Subproject commit c12733a4712e802b41fd26ad8df7475d06c334b3
|
||||
@ -164,6 +164,7 @@ class PackageModel(QAbstractListModel):
|
||||
|
||||
def update_tot(self, tot: int) -> None:
|
||||
self.package.time_over_target = tot
|
||||
self.layoutChanged.emit()
|
||||
|
||||
@property
|
||||
def mission_target(self) -> MissionTarget:
|
||||
@ -234,7 +235,7 @@ class AtoModel(QAbstractListModel):
|
||||
"""Returns the package at the given index."""
|
||||
return self.ato.packages[index.row()]
|
||||
|
||||
def replace_from_game(self, game: Optional[Game]) -> None:
|
||||
def replace_from_game(self, game: Optional[Game], player: bool) -> 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
|
||||
@ -244,7 +245,10 @@ class AtoModel(QAbstractListModel):
|
||||
self.game = game
|
||||
self.package_models.clear()
|
||||
if self.game is not None:
|
||||
self.ato = game.blue_ato
|
||||
if player:
|
||||
self.ato = game.blue_ato
|
||||
else:
|
||||
self.ato = game.red_ato
|
||||
else:
|
||||
self.ato = AirTaskingOrder()
|
||||
self.endResetModel()
|
||||
@ -268,8 +272,8 @@ class GameModel:
|
||||
"""
|
||||
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())
|
||||
self.red_ato_model = AtoModel(self.game, AirTaskingOrder())
|
||||
|
||||
def set(self, game: Optional[Game]) -> None:
|
||||
"""Updates the managed Game object.
|
||||
@ -280,4 +284,5 @@ class GameModel:
|
||||
loaded.
|
||||
"""
|
||||
self.game = game
|
||||
self.ato_model.replace_from_game(self.game)
|
||||
self.ato_model.replace_from_game(self.game, player=True)
|
||||
self.red_ato_model.replace_from_game(self.game, player=False)
|
||||
|
||||
@ -6,7 +6,7 @@ from PySide2.QtGui import QColor, QFont, QPixmap
|
||||
from theater.theatergroundobject import CATEGORY_MAP
|
||||
from .liberation_theme import get_theme_icons
|
||||
|
||||
VERSION_STRING = "2.1.4"
|
||||
VERSION_STRING = "2.2.0-preview"
|
||||
|
||||
URLS : Dict[str, str] = {
|
||||
"Manual": "https://github.com/khopa/dcs_liberation/wiki",
|
||||
@ -40,6 +40,7 @@ COLORS: Dict[str, QColor] = {
|
||||
"light_blue": QColor(105, 182, 240, 90),
|
||||
"blue": QColor(0, 132, 255),
|
||||
"dark_blue": QColor(45, 62, 80),
|
||||
"sea_blue": QColor(52, 68, 85),
|
||||
"blue_transparent": QColor(0, 132, 255, 20),
|
||||
|
||||
"purple": QColor(187, 137, 255),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
@ -11,6 +11,8 @@ from PySide2.QtWidgets import (
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game
|
||||
from game.event import CAP, CAS, FrontlineAttackEvent
|
||||
from gen.ato import Package
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
||||
@ -95,7 +97,7 @@ class QTopPanel(QFrame):
|
||||
if game is None:
|
||||
return
|
||||
|
||||
self.turnCounter.setCurrentTurn(game.turn, game.current_day)
|
||||
self.turnCounter.setCurrentTurn(game.turn, game.conditions)
|
||||
self.budgetBox.setGame(game)
|
||||
self.factionsInfos.setGame(game)
|
||||
|
||||
@ -117,6 +119,24 @@ class QTopPanel(QFrame):
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
self.proceedButton.setEnabled(True)
|
||||
|
||||
def negative_start_packages(self) -> List[Package]:
|
||||
packages = []
|
||||
for package in self.game_model.ato_model.ato.packages:
|
||||
if not package.flights:
|
||||
continue
|
||||
estimator = TotEstimator(package)
|
||||
for flight in package.flights:
|
||||
if estimator.mission_start_time(flight) < 0:
|
||||
packages.append(package)
|
||||
break
|
||||
return packages
|
||||
|
||||
@staticmethod
|
||||
def fix_tots(packages: List[Package]) -> None:
|
||||
for package in packages:
|
||||
estimator = TotEstimator(package)
|
||||
package.time_over_target = estimator.earliest_tot()
|
||||
|
||||
def ato_has_clients(self) -> bool:
|
||||
for package in self.game.blue_ato.packages:
|
||||
for flight in package.flights:
|
||||
@ -142,12 +162,52 @@ class QTopPanel(QFrame):
|
||||
)
|
||||
return result == QMessageBox.Yes
|
||||
|
||||
def confirm_negative_start_time(self,
|
||||
negative_starts: List[Package]) -> bool:
|
||||
formatted = '<br />'.join(
|
||||
[f"{p.primary_task.name} {p.target.name}" for p in negative_starts]
|
||||
)
|
||||
mbox = QMessageBox(
|
||||
QMessageBox.Question,
|
||||
"Continue with past start times?",
|
||||
("Some flights in the following packages have start times set "
|
||||
"earlier than mission start time:<br />"
|
||||
"<br />"
|
||||
f"{formatted}<br />"
|
||||
"<br />"
|
||||
"Flight start times are estimated based on the package TOT, so it "
|
||||
"is possible that not all flights will be able to reach the "
|
||||
"target area at their assigned times.<br />"
|
||||
"<br />"
|
||||
"You can either continue with the mission as planned, with the "
|
||||
"misplanned flights potentially flying too fast and/or missing "
|
||||
"their rendezvous; automatically fix negative TOTs; or cancel "
|
||||
"mission start and fix the packages manually."),
|
||||
parent=self
|
||||
)
|
||||
auto = mbox.addButton("Fix TOTs automatically", QMessageBox.ActionRole)
|
||||
ignore = mbox.addButton("Continue without fixing",
|
||||
QMessageBox.DestructiveRole)
|
||||
cancel = mbox.addButton(QMessageBox.Cancel)
|
||||
mbox.setEscapeButton(cancel)
|
||||
mbox.exec_()
|
||||
clicked = mbox.clickedButton()
|
||||
if clicked == auto:
|
||||
self.fix_tots(negative_starts)
|
||||
return True
|
||||
elif clicked == ignore:
|
||||
return True
|
||||
return False
|
||||
|
||||
def launch_mission(self):
|
||||
"""Finishes planning and waits for mission completion."""
|
||||
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
||||
return
|
||||
|
||||
# TODO: Verify no negative start times.
|
||||
negative_starts = self.negative_start_packages()
|
||||
if negative_starts:
|
||||
if not self.confirm_negative_start_time(negative_starts):
|
||||
return
|
||||
|
||||
# TODO: Refactor this nonsense.
|
||||
game_event = None
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import datetime
|
||||
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
|
||||
|
||||
from game.weather import Conditions, TimeOfDay
|
||||
import qt_ui.uiconstants as CONST
|
||||
|
||||
|
||||
@ -13,23 +14,37 @@ class QTurnCounter(QGroupBox):
|
||||
def __init__(self):
|
||||
super(QTurnCounter, self).__init__("Turn")
|
||||
|
||||
self.icons = [CONST.ICONS["Dawn"], CONST.ICONS["Day"], CONST.ICONS["Dusk"], CONST.ICONS["Night"]]
|
||||
|
||||
self.daytime_icon = QLabel()
|
||||
self.daytime_icon.setPixmap(self.icons[0])
|
||||
self.turn_info = QLabel()
|
||||
self.icons = {
|
||||
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
|
||||
TimeOfDay.Day: CONST.ICONS["Day"],
|
||||
TimeOfDay.Dusk: CONST.ICONS["Dusk"],
|
||||
TimeOfDay.Night: CONST.ICONS["Night"],
|
||||
}
|
||||
|
||||
self.layout = QHBoxLayout()
|
||||
self.layout.addWidget(self.daytime_icon)
|
||||
self.layout.addWidget(self.turn_info)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def setCurrentTurn(self, turn: int, current_day: datetime):
|
||||
self.daytime_icon = QLabel()
|
||||
self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn])
|
||||
self.layout.addWidget(self.daytime_icon)
|
||||
|
||||
self.time_column = QVBoxLayout()
|
||||
self.layout.addLayout(self.time_column)
|
||||
|
||||
self.date_display = QLabel()
|
||||
self.time_column.addWidget(self.date_display)
|
||||
|
||||
self.time_display = QLabel()
|
||||
self.time_column.addWidget(self.time_display)
|
||||
|
||||
def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
|
||||
"""Sets the turn information display.
|
||||
|
||||
:arg turn Current turn number.
|
||||
:arg conditions Current time and weather conditions.
|
||||
"""
|
||||
Set the money amount to display
|
||||
:arg turn Current turn number
|
||||
:arg current_day Current day
|
||||
"""
|
||||
self.daytime_icon.setPixmap(self.icons[turn % 4])
|
||||
self.turn_info.setText(current_day.strftime("%d %b %Y"))
|
||||
self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
|
||||
self.date_display.setText(conditions.start_time.strftime("%d %b %Y"))
|
||||
self.time_display.setText(
|
||||
conditions.start_time.strftime("%H:%M:%S Local"))
|
||||
self.setTitle("Turn " + str(turn + 1))
|
||||
|
||||
@ -19,7 +19,6 @@ class QFlightTypeComboBox(QComboBox):
|
||||
|
||||
COMMON_ENEMY_MISSIONS = [
|
||||
FlightType.ESCORT,
|
||||
FlightType.TARCAP,
|
||||
FlightType.SEAD,
|
||||
FlightType.DEAD,
|
||||
# TODO: FlightType.ELINT,
|
||||
@ -27,42 +26,46 @@ class QFlightTypeComboBox(QComboBox):
|
||||
# TODO: FlightType.RECON,
|
||||
]
|
||||
|
||||
FRIENDLY_AIRBASE_MISSIONS = [
|
||||
FlightType.CAP,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
COMMON_FRIENDLY_MISSIONS = [
|
||||
FlightType.BARCAP,
|
||||
]
|
||||
|
||||
FRIENDLY_AIRBASE_MISSIONS = [
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
] + COMMON_FRIENDLY_MISSIONS
|
||||
|
||||
FRIENDLY_CARRIER_MISSIONS = [
|
||||
FlightType.BARCAP,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: Buddy tanking for the A-4?
|
||||
# TODO: Rescue chopper?
|
||||
# TODO: Inter-ship logistics?
|
||||
]
|
||||
] + COMMON_FRIENDLY_MISSIONS
|
||||
|
||||
ENEMY_CARRIER_MISSIONS = [
|
||||
FlightType.ESCORT,
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
# TODO: FlightType.ANTISHIP
|
||||
]
|
||||
|
||||
ENEMY_AIRBASE_MISSIONS = [
|
||||
FlightType.BARCAP,
|
||||
# TODO: FlightType.STRIKE
|
||||
] + COMMON_ENEMY_MISSIONS
|
||||
|
||||
FRIENDLY_GROUND_OBJECT_MISSIONS = [
|
||||
FlightType.CAP,
|
||||
# TODO: FlightType.LOGISTICS
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
]
|
||||
] + COMMON_FRIENDLY_MISSIONS
|
||||
|
||||
ENEMY_GROUND_OBJECT_MISSIONS = [
|
||||
FlightType.BARCAP,
|
||||
FlightType.STRIKE,
|
||||
] + COMMON_ENEMY_MISSIONS
|
||||
|
||||
FRONT_LINE_MISSIONS = [
|
||||
FlightType.CAS,
|
||||
FlightType.TARCAP,
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
# TODO: FlightType.EVAC
|
||||
] + COMMON_ENEMY_MISSIONS
|
||||
|
||||
@ -21,6 +21,7 @@ from game import Game, db
|
||||
from game.data.aaa_db import AAA_UNITS
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.utils import meter_to_feet
|
||||
from game.weather import TimeOfDay
|
||||
from gen import Conflict, PackageWaypointTiming
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
@ -55,25 +56,42 @@ class QLiberationMap(QGraphicsView):
|
||||
self.factor = 1
|
||||
self.factorized = 1
|
||||
self.init_scene()
|
||||
self.connectSignals()
|
||||
self.setGame(game_model.game)
|
||||
|
||||
GameUpdateSignal.get_instance().flight_paths_changed.connect(
|
||||
lambda: self.draw_flight_plans(self.scene())
|
||||
)
|
||||
|
||||
def update_package_selection(index: Optional[int]) -> None:
|
||||
self.selected_flight = index, 0
|
||||
def update_package_selection(index: int) -> None:
|
||||
# Optional[int] isn't a valid type for a Qt signal. None will be
|
||||
# converted to zero automatically. We use -1 to indicate no
|
||||
# selection.
|
||||
if index == -1:
|
||||
self.selected_flight = None
|
||||
else:
|
||||
self.selected_flight = index, 0
|
||||
self.draw_flight_plans(self.scene())
|
||||
|
||||
GameUpdateSignal.get_instance().package_selection_changed.connect(
|
||||
update_package_selection
|
||||
)
|
||||
|
||||
def update_flight_selection(index: Optional[int]) -> None:
|
||||
def update_flight_selection(index: int) -> None:
|
||||
if self.selected_flight is None:
|
||||
logging.error("Flight was selected with no package selected")
|
||||
if index != -1:
|
||||
# We don't know what order update_package_selection and
|
||||
# update_flight_selection will be called in when the last
|
||||
# package is removed. If no flight is selected, it's not a
|
||||
# problem to also have no package selected.
|
||||
logging.error(
|
||||
"Flight was selected with no package selected")
|
||||
return
|
||||
|
||||
# Optional[int] isn't a valid type for a Qt signal. None will be
|
||||
# converted to zero automatically. We use -1 to indicate no
|
||||
# selection.
|
||||
if index == -1:
|
||||
self.selected_flight = self.selected_flight[0], None
|
||||
self.selected_flight = self.selected_flight[0], index
|
||||
self.draw_flight_plans(self.scene())
|
||||
|
||||
@ -90,9 +108,6 @@ class QLiberationMap(QGraphicsView):
|
||||
self.setFrameShape(QFrame.NoFrame)
|
||||
self.setDragMode(QGraphicsView.ScrollHandDrag)
|
||||
|
||||
def connectSignals(self):
|
||||
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
|
||||
|
||||
def setGame(self, game: Optional[Game]):
|
||||
self.game = game
|
||||
logging.debug("Reloading Map Canvas")
|
||||
@ -244,7 +259,7 @@ 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:
|
||||
def clear_flight_paths(self, scene: QGraphicsScene) -> None:
|
||||
for item in self.flight_path_items:
|
||||
try:
|
||||
scene.removeItem(item)
|
||||
@ -252,12 +267,20 @@ class QLiberationMap(QGraphicsView):
|
||||
# Something may have caused those items to already be removed.
|
||||
pass
|
||||
self.flight_path_items.clear()
|
||||
|
||||
def draw_flight_plans(self, scene: QGraphicsScene) -> None:
|
||||
self.clear_flight_paths(scene)
|
||||
if DisplayOptions.flight_paths.hide:
|
||||
return
|
||||
packages = list(self.game_model.ato_model.packages)
|
||||
if self.game.settings.show_red_ato:
|
||||
packages.extend(self.game_model.red_ato_model.packages)
|
||||
for p_idx, package_model in enumerate(packages):
|
||||
for f_idx, flight in enumerate(package_model.flights):
|
||||
selected = (p_idx, f_idx) == self.selected_flight
|
||||
if self.selected_flight is None:
|
||||
selected = False
|
||||
else:
|
||||
selected = (p_idx, f_idx) == self.selected_flight
|
||||
if DisplayOptions.flight_paths.only_selected and not selected:
|
||||
continue
|
||||
self.draw_flight_plan(scene, package_model.package, flight,
|
||||
@ -486,9 +509,9 @@ class QLiberationMap(QGraphicsView):
|
||||
scene.addPixmap(bg)
|
||||
|
||||
# Apply graphical effects to simulate current daytime
|
||||
if self.game.current_turn_daytime == "day":
|
||||
if self.game.current_turn_time_of_day == TimeOfDay.Day:
|
||||
pass
|
||||
elif self.game.current_turn_daytime == "night":
|
||||
elif self.game.current_turn_time_of_day == TimeOfDay.Night:
|
||||
ov = QPixmap(bg.width(), bg.height())
|
||||
ov.fill(CONST.COLORS["night_overlay"])
|
||||
overlay = scene.addPixmap(ov)
|
||||
@ -507,6 +530,11 @@ class QLiberationMap(QGraphicsView):
|
||||
# Polygon display mode
|
||||
if self.game.theater.landmap is not None:
|
||||
|
||||
for sea_zone in self.game.theater.landmap[2]:
|
||||
print(sea_zone)
|
||||
poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in sea_zone])
|
||||
scene.addPolygon(poly, CONST.COLORS["sea_blue"], CONST.COLORS["sea_blue"])
|
||||
|
||||
for inclusion_zone in self.game.theater.landmap[0]:
|
||||
poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in inclusion_zone])
|
||||
scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_grey"])
|
||||
@ -516,3 +544,5 @@ class QLiberationMap(QGraphicsView):
|
||||
scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_dark_grey"])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ from PySide2.QtWidgets import QGraphicsItem
|
||||
import qt_ui.uiconstants as const
|
||||
from game import Game
|
||||
from game.data.building_data import FORTIFICATION_BUILDINGS
|
||||
from game.db import REWARDS
|
||||
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
||||
from theater import ControlPoint, TheaterGroundObject
|
||||
from .QMapObject import QMapObject
|
||||
@ -27,7 +28,14 @@ class QMapGroundObject(QMapObject):
|
||||
self.buildings = buildings if buildings is not None else []
|
||||
self.setFlag(QGraphicsItem.ItemIgnoresTransformations, False)
|
||||
self.ground_object_dialog: Optional[QGroundObjectMenu] = None
|
||||
self.setToolTip(self.tooltip)
|
||||
|
||||
@property
|
||||
def tooltip(self) -> str:
|
||||
lines = [
|
||||
f"[{self.ground_object.obj_name}]",
|
||||
f"${self.production_per_turn} per turn",
|
||||
]
|
||||
if self.ground_object.groups:
|
||||
units = {}
|
||||
for g in self.ground_object.groups:
|
||||
@ -36,16 +44,23 @@ class QMapGroundObject(QMapObject):
|
||||
units[u.type] = units[u.type]+1
|
||||
else:
|
||||
units[u.type] = 1
|
||||
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])
|
||||
lines.append(f"{unit} x {units[unit]}")
|
||||
else:
|
||||
tooltip = "[" + self.ground_object.obj_name + "]" + "\n"
|
||||
for building in buildings:
|
||||
for building in self.buildings:
|
||||
if not building.is_dead:
|
||||
tooltip = tooltip + str(building.dcs_identifier) + "\n"
|
||||
self.setToolTip(tooltip[:-1])
|
||||
lines.append(f"{building.dcs_identifier}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@property
|
||||
def production_per_turn(self) -> int:
|
||||
production = 0
|
||||
for g in self.control_point.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
production += REWARDS[g.category]
|
||||
return production
|
||||
|
||||
def paint(self, painter, option, widget=None) -> None:
|
||||
player_icons = "_blue"
|
||||
|
||||
@ -24,8 +24,8 @@ class GameUpdateSignal(QObject):
|
||||
debriefingReceived = Signal(DebriefingSignal)
|
||||
|
||||
flight_paths_changed = Signal()
|
||||
package_selection_changed = Signal(int) # Optional[int]
|
||||
flight_selection_changed = Signal(int) # Optional[int]
|
||||
package_selection_changed = Signal(int) # -1 indicates no selection.
|
||||
flight_selection_changed = Signal(int) # -1 indicates no selection.
|
||||
|
||||
def __init__(self):
|
||||
super(GameUpdateSignal, self).__init__()
|
||||
@ -33,11 +33,11 @@ class GameUpdateSignal(QObject):
|
||||
|
||||
def select_package(self, index: Optional[int]) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_selection_changed.emit(index)
|
||||
self.package_selection_changed.emit(-1 if index is None else index)
|
||||
|
||||
def select_flight(self, index: Optional[int]) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_selection_changed.emit(index)
|
||||
self.flight_selection_changed.emit(-1 if index is None else index)
|
||||
|
||||
def redraw_flight_paths(self) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
|
||||
@ -43,6 +43,7 @@ class QLiberationWindow(QMainWindow):
|
||||
Dialog.set_game(self.game_model)
|
||||
self.ato_panel = None
|
||||
self.info_panel = None
|
||||
self.liberation_map = None
|
||||
self.setGame(persistency.restore_game())
|
||||
|
||||
self.setGeometry(300, 100, 270, 100)
|
||||
@ -224,9 +225,11 @@ class QLiberationWindow(QMainWindow):
|
||||
if game is not None:
|
||||
game.on_load()
|
||||
self.game = game
|
||||
if self.info_panel:
|
||||
if self.info_panel is not None:
|
||||
self.info_panel.setGame(game)
|
||||
self.game_model.set(self.game)
|
||||
if self.liberation_map is not None:
|
||||
self.liberation_map.setGame(game)
|
||||
|
||||
def showAboutDialog(self):
|
||||
text = "<h3>DCS Liberation " + CONST.VERSION_STRING + "</h3>" + \
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, Set
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game.event.event import UnitsDeliveryEvent
|
||||
from qt_ui.models import GameModel
|
||||
@ -48,26 +49,27 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
def init_ui(self):
|
||||
main_layout = QVBoxLayout()
|
||||
|
||||
units = {
|
||||
CAP: db.find_unittype(CAP, self.game_model.game.player_name),
|
||||
CAS: db.find_unittype(CAS, self.game_model.game.player_name),
|
||||
}
|
||||
tasks = [CAP, CAS]
|
||||
|
||||
scroll_content = QWidget()
|
||||
task_box_layout = QGridLayout()
|
||||
row = 0
|
||||
|
||||
for task_type in units.keys():
|
||||
units_column = list(set(units[task_type]))
|
||||
if len(units_column) == 0:
|
||||
unit_types: Set[UnitType] = set()
|
||||
for task in tasks:
|
||||
units = db.find_unittype(task, self.game_model.game.player_name)
|
||||
if not units:
|
||||
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:
|
||||
for unit in units:
|
||||
if self.cp.is_carrier and unit not in db.CARRIER_CAPABLE:
|
||||
continue
|
||||
if self.cp.is_lha and not unit_type in db.LHA_CAPABLE:
|
||||
if self.cp.is_lha and unit not in db.LHA_CAPABLE:
|
||||
continue
|
||||
row = self.add_purchase_row(unit_type, task_box_layout, row)
|
||||
unit_types.add(unit)
|
||||
|
||||
sorted_units = sorted(unit_types, key=lambda u: db.unit_type_name_2(u))
|
||||
for unit_type in sorted_units:
|
||||
row = self.add_purchase_row(unit_type, task_box_layout, row)
|
||||
stretch = QVBoxLayout()
|
||||
stretch.addStretch()
|
||||
task_box_layout.addLayout(stretch, row, 0)
|
||||
|
||||
@ -16,6 +16,7 @@ from game.game import Game
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.models import AtoModel, PackageModel
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.widgets.ato import QFlightList
|
||||
@ -34,12 +35,6 @@ class QPackageDialog(QDialog):
|
||||
#: 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
|
||||
@ -77,20 +72,20 @@ class QPackageDialog(QDialog):
|
||||
self.tot_label = QLabel("Time Over Target:")
|
||||
self.tot_column.addWidget(self.tot_label)
|
||||
|
||||
if self.package_model.package.time_over_target is None:
|
||||
time = None
|
||||
else:
|
||||
delay = self.package_model.package.time_over_target
|
||||
hours = delay // 3600
|
||||
minutes = delay // 60 % 60
|
||||
seconds = delay % 60
|
||||
time = QTime(hours, minutes, seconds)
|
||||
|
||||
self.tot_spinner = QTimeEdit(time)
|
||||
self.tot_spinner = QTimeEdit(self.tot_qtime())
|
||||
self.tot_spinner.setMinimumTime(QTime(0, 0))
|
||||
self.tot_spinner.setDisplayFormat("T+hh:mm:ss")
|
||||
self.tot_spinner.timeChanged.connect(self.save_tot)
|
||||
self.tot_column.addWidget(self.tot_spinner)
|
||||
|
||||
self.reset_tot_button = QPushButton("Reset TOT")
|
||||
self.reset_tot_button.setToolTip(
|
||||
"Sets the package TOT to the earliest time that all flights can "
|
||||
"arrive at the target."
|
||||
)
|
||||
self.reset_tot_button.clicked.connect(self.reset_tot)
|
||||
self.tot_column.addWidget(self.reset_tot_button)
|
||||
|
||||
self.package_view = QFlightList(self.package_model)
|
||||
self.package_view.selectionModel().selectionChanged.connect(
|
||||
self.on_selection_changed
|
||||
@ -114,17 +109,40 @@ class QPackageDialog(QDialog):
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.accepted.connect(self.on_save)
|
||||
self.finished.connect(self.on_close)
|
||||
self.rejected.connect(self.on_cancel)
|
||||
|
||||
def tot_qtime(self) -> QTime:
|
||||
delay = self.package_model.package.time_over_target
|
||||
hours = delay // 3600
|
||||
minutes = delay // 60 % 60
|
||||
seconds = delay % 60
|
||||
return QTime(hours, minutes, seconds)
|
||||
|
||||
def on_cancel(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def on_close(_result) -> None:
|
||||
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||
|
||||
def on_save(self) -> None:
|
||||
self.save_tot()
|
||||
|
||||
def save_tot(self) -> None:
|
||||
time = self.tot_spinner.time()
|
||||
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
||||
self.package_model.update_tot(seconds)
|
||||
|
||||
def reset_tot(self) -> None:
|
||||
if not list(self.package_model.flights):
|
||||
self.package_model.update_tot(0)
|
||||
else:
|
||||
self.package_model.update_tot(
|
||||
TotEstimator(self.package_model.package).earliest_tot())
|
||||
self.tot_spinner.setTime(self.tot_qtime())
|
||||
|
||||
def on_selection_changed(self, selected: QItemSelection,
|
||||
_deselected: QItemSelection) -> None:
|
||||
"""Updates the state of the delete button."""
|
||||
@ -139,14 +157,13 @@ class QPackageDialog(QDialog):
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds the new flight to the package."""
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
self.package_model.add_flight(flight)
|
||||
planner = FlightPlanBuilder(self.game, self.package_model.package,
|
||||
is_player=True)
|
||||
planner.populate_flight_plan(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."""
|
||||
@ -154,11 +171,10 @@ class QPackageDialog(QDialog):
|
||||
if flight is None:
|
||||
logging.error(f"Cannot delete flight when no flight is selected.")
|
||||
return
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
self.package_model.delete_flight(flight)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_changed.emit()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.flight_removed.emit(flight)
|
||||
|
||||
|
||||
class QNewPackageDialog(QPackageDialog):
|
||||
@ -174,22 +190,22 @@ class QNewPackageDialog(QPackageDialog):
|
||||
|
||||
self.save_button = QPushButton("Save")
|
||||
self.save_button.setProperty("style", "start-button")
|
||||
self.save_button.clicked.connect(self.on_save)
|
||||
self.save_button.clicked.connect(self.accept)
|
||||
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.save_tot()
|
||||
super().on_save()
|
||||
self.ato_model.add_package(self.package_model.package)
|
||||
|
||||
def on_cancel(self) -> None:
|
||||
super().on_cancel()
|
||||
for flight in self.package_model.package.flights:
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
self.close()
|
||||
self.game_model.game.aircraft_inventory.return_from_flight(flight)
|
||||
|
||||
|
||||
class QEditPackageDialog(QPackageDialog):
|
||||
@ -210,30 +226,9 @@ class QEditPackageDialog(QPackageDialog):
|
||||
|
||||
self.done_button = QPushButton("Done")
|
||||
self.done_button.setProperty("style", "start-button")
|
||||
self.done_button.clicked.connect(self.on_done)
|
||||
self.done_button.clicked.connect(self.accept)
|
||||
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.save_tot()
|
||||
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.
|
||||
|
||||
@ -19,11 +19,15 @@ class QFlightPlanner(QTabWidget):
|
||||
def __init__(self, package: Package, flight: Flight, game: Game):
|
||||
super().__init__()
|
||||
|
||||
self.general_settings_tab = QGeneralFlightSettingsTab(game, flight)
|
||||
self.general_settings_tab = QGeneralFlightSettingsTab(
|
||||
game, package, flight
|
||||
)
|
||||
# noinspection PyUnresolvedReferences
|
||||
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, package, flight)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.waypoint_tab.on_flight_changed.connect(
|
||||
lambda: self.on_planned_flight_changed.emit())
|
||||
self.addTab(self.general_settings_tab, "General Flight settings")
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
import datetime
|
||||
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
|
||||
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
|
||||
|
||||
# TODO: Remove?
|
||||
class QFlightDepartureDisplay(QGroupBox):
|
||||
|
||||
def __init__(self, package: Package, flight: Flight):
|
||||
super().__init__("Departure")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
departure_row = QHBoxLayout()
|
||||
layout.addLayout(departure_row)
|
||||
|
||||
estimator = TotEstimator(package)
|
||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||
|
||||
departure_row.addWidget(QLabel(
|
||||
f"Departing from <b>{flight.from_cp.name}</b>"
|
||||
))
|
||||
departure_row.addWidget(QLabel(f"At T+{delay}"))
|
||||
|
||||
layout.addWidget(QLabel("Determined based on the package TOT. Edit the "
|
||||
"package to adjust the TOT."))
|
||||
|
||||
self.setLayout(layout)
|
||||
@ -1,31 +0,0 @@
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox
|
||||
|
||||
|
||||
# TODO: Remove?
|
||||
class QFlightDepartureEditor(QGroupBox):
|
||||
|
||||
def __init__(self, flight):
|
||||
super(QFlightDepartureEditor, self).__init__("Departure")
|
||||
self.flight = flight
|
||||
|
||||
layout = QHBoxLayout()
|
||||
self.depart_from = QLabel("Departing from <b>" + self.flight.from_cp.name + "</b>")
|
||||
self.depart_at_t = QLabel("At T +")
|
||||
self.minutes = QLabel(" minutes")
|
||||
|
||||
self.departure_delta = QSpinBox(self)
|
||||
self.departure_delta.setMinimum(0)
|
||||
self.departure_delta.setMaximum(120)
|
||||
self.departure_delta.setValue(self.flight.scheduled_in // 60)
|
||||
self.departure_delta.valueChanged.connect(self.change_scheduled)
|
||||
|
||||
layout.addWidget(self.depart_from)
|
||||
layout.addWidget(self.depart_at_t)
|
||||
layout.addWidget(self.departure_delta)
|
||||
layout.addWidget(self.minutes)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.changed = self.departure_delta.valueChanged
|
||||
|
||||
def change_scheduled(self):
|
||||
self.flight.scheduled_in = int(self.departure_delta.value() * 60)
|
||||
@ -2,26 +2,29 @@ from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import QFrame, QGridLayout, QVBoxLayout
|
||||
|
||||
from game import Game
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.windows.mission.flight.settings.QFlightDepartureEditor import QFlightDepartureEditor
|
||||
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import QFlightSlotEditor
|
||||
from qt_ui.windows.mission.flight.settings.QFlightStartType import QFlightStartType
|
||||
from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import QFlightTypeTaskInfo
|
||||
from qt_ui.windows.mission.flight.settings.QFlightDepartureDisplay import \
|
||||
QFlightDepartureDisplay
|
||||
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import \
|
||||
QFlightSlotEditor
|
||||
from qt_ui.windows.mission.flight.settings.QFlightStartType import \
|
||||
QFlightStartType
|
||||
from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import \
|
||||
QFlightTypeTaskInfo
|
||||
|
||||
|
||||
class QGeneralFlightSettingsTab(QFrame):
|
||||
on_flight_settings_changed = Signal()
|
||||
|
||||
def __init__(self, game: Game, flight: Flight):
|
||||
super(QGeneralFlightSettingsTab, self).__init__()
|
||||
self.flight = flight
|
||||
self.game = game
|
||||
def __init__(self, game: Game, package: Package, flight: Flight):
|
||||
super().__init__()
|
||||
|
||||
layout = QGridLayout()
|
||||
flight_info = QFlightTypeTaskInfo(self.flight)
|
||||
flight_departure = QFlightDepartureEditor(self.flight)
|
||||
flight_slots = QFlightSlotEditor(self.flight, self.game)
|
||||
flight_start_type = QFlightStartType(self.flight)
|
||||
flight_info = QFlightTypeTaskInfo(flight)
|
||||
flight_departure = QFlightDepartureDisplay(package, flight)
|
||||
flight_slots = QFlightSlotEditor(flight, game)
|
||||
flight_start_type = QFlightStartType(flight)
|
||||
layout.addWidget(flight_info, 0, 0)
|
||||
layout.addWidget(flight_departure, 1, 0)
|
||||
layout.addWidget(flight_slots, 2, 0)
|
||||
@ -31,8 +34,6 @@ class QGeneralFlightSettingsTab(QFrame):
|
||||
layout.addLayout(vstretch, 3, 0)
|
||||
self.setLayout(layout)
|
||||
|
||||
flight_start_type.setEnabled(self.flight.client_count > 0)
|
||||
flight_start_type.setEnabled(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())
|
||||
lambda: flight_start_type.setEnabled(flight.client_count > 0))
|
||||
|
||||
@ -54,6 +54,7 @@ class QFlightWaypointTab(QFrame):
|
||||
rlayout.addWidget(QLabel("<strong>Generator :</strong>"))
|
||||
rlayout.addWidget(QLabel("<small>AI compatible</small>"))
|
||||
|
||||
# TODO: Filter by objective type.
|
||||
self.recreate_buttons.clear()
|
||||
recreate_types = [
|
||||
FlightType.CAS,
|
||||
@ -137,13 +138,16 @@ class QFlightWaypointTab(QFrame):
|
||||
QMessageBox.Yes
|
||||
)
|
||||
if result == QMessageBox.Yes:
|
||||
# TODO: These should all be just CAP.
|
||||
# TODO: Should be buttons for both BARCAP and TARCAP.
|
||||
# BARCAP and TARCAP behave differently. TARCAP arrives a few minutes
|
||||
# ahead of the rest of the package and stays until the package
|
||||
# departs, whereas BARCAP usually isn't part of a strike package and
|
||||
# has a fixed mission time.
|
||||
if task == FlightType.CAP:
|
||||
if isinstance(self.package.target, FrontLine):
|
||||
task = FlightType.TARCAP
|
||||
elif isinstance(self.package.target, ControlPoint):
|
||||
if self.package.target.is_fleet:
|
||||
task = FlightType.BARCAP
|
||||
task = FlightType.BARCAP
|
||||
self.flight.flight_type = task
|
||||
self.planner.populate_flight_plan(self.flight)
|
||||
self.flight_waypoint_list.update_list()
|
||||
|
||||
@ -1,18 +1,50 @@
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from PySide2.QtCore import QSize, Qt, QItemSelectionModel, QPoint
|
||||
from PySide2.QtGui import QStandardItemModel, QStandardItem
|
||||
from PySide2.QtWidgets import QLabel, QDialog, QGridLayout, QListView, QStackedLayout, QComboBox, QWidget, \
|
||||
QAbstractItemView, QPushButton, QGroupBox, QCheckBox, QVBoxLayout, QSpinBox
|
||||
from PySide2.QtWidgets import (
|
||||
QLabel,
|
||||
QDialog,
|
||||
QGridLayout,
|
||||
QListView,
|
||||
QStackedLayout,
|
||||
QComboBox,
|
||||
QWidget,
|
||||
QAbstractItemView,
|
||||
QPushButton,
|
||||
QGroupBox,
|
||||
QCheckBox,
|
||||
QVBoxLayout,
|
||||
QSpinBox,
|
||||
)
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.game import Game
|
||||
from game.infos.information import Information
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine
|
||||
from plugin import LuaPluginManager
|
||||
|
||||
class CheatSettingsBox(QGroupBox):
|
||||
def __init__(self, game: Game, apply_settings: Callable[[], None]) -> None:
|
||||
super().__init__("Cheat Settings")
|
||||
self.main_layout = QVBoxLayout()
|
||||
self.setLayout(self.main_layout)
|
||||
|
||||
self.red_ato_checkbox = QCheckBox()
|
||||
self.red_ato_checkbox.setChecked(game.settings.show_red_ato)
|
||||
self.red_ato_checkbox.toggled.connect(apply_settings)
|
||||
self.red_ato = QLabeledWidget("Show Red ATO:", self.red_ato_checkbox)
|
||||
self.main_layout.addLayout(self.red_ato)
|
||||
|
||||
@property
|
||||
def show_red_ato(self) -> bool:
|
||||
return self.red_ato_checkbox.isChecked()
|
||||
|
||||
|
||||
class QSettingsWindow(QDialog):
|
||||
|
||||
def __init__(self, game: Game):
|
||||
@ -262,9 +294,12 @@ class QSettingsWindow(QDialog):
|
||||
def initCheatLayout(self):
|
||||
|
||||
self.cheatPage = QWidget()
|
||||
self.cheatLayout = QGridLayout()
|
||||
self.cheatLayout = QVBoxLayout()
|
||||
self.cheatPage.setLayout(self.cheatLayout)
|
||||
|
||||
self.cheat_options = CheatSettingsBox(self.game, self.applySettings)
|
||||
self.cheatLayout.addWidget(self.cheat_options)
|
||||
|
||||
self.moneyCheatBox = QGroupBox("Money Cheat")
|
||||
self.moneyCheatBox.setAlignment(Qt.AlignTop)
|
||||
self.moneyCheatBoxLayout = QGridLayout()
|
||||
@ -280,7 +315,7 @@ class QSettingsWindow(QDialog):
|
||||
btn.setProperty("style", "btn-danger")
|
||||
btn.clicked.connect(self.cheatLambda(amount))
|
||||
self.moneyCheatBoxLayout.addWidget(btn, i/2, i%2)
|
||||
self.cheatLayout.addWidget(self.moneyCheatBox, 0, 0)
|
||||
self.cheatLayout.addWidget(self.moneyCheatBox, stretch=1)
|
||||
|
||||
def initPluginsLayout(self):
|
||||
uiPrepared = False
|
||||
@ -332,8 +367,6 @@ class QSettingsWindow(QDialog):
|
||||
self.game.settings.external_views_allowed = self.ext_views.isChecked()
|
||||
self.game.settings.generate_marks = self.generate_marks.isChecked()
|
||||
|
||||
print(self.game.settings.map_coalition_visibility)
|
||||
|
||||
self.game.settings.supercarrier = self.supercarrier.isChecked()
|
||||
|
||||
self.game.settings.perf_red_alert_state = self.red_alert.isChecked()
|
||||
@ -347,6 +380,8 @@ 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.show_red_ato = self.cheat_options.show_red_ato
|
||||
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def onSelectionChanged(self):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,15 +1,15 @@
|
||||
import pickle
|
||||
|
||||
from dcs.mission import Mission
|
||||
from dcs.planes import A_10C
|
||||
|
||||
for terrain in ["cau"]:
|
||||
for terrain in ["cau", "nev", "syria", "channel", "normandy", "gulf"]:
|
||||
print("Terrain " + terrain)
|
||||
m = Mission()
|
||||
m.load_file("./{}_terrain.miz".format(terrain))
|
||||
|
||||
inclusion_zones = []
|
||||
exclusion_zones = []
|
||||
seas_zones = []
|
||||
for plane_group in m.country("USA").plane_group:
|
||||
zone = [(x.position.x, x.position.y) for x in plane_group.points]
|
||||
|
||||
@ -22,6 +22,10 @@ for terrain in ["cau"]:
|
||||
else:
|
||||
inclusion_zones.append(zone)
|
||||
|
||||
for ship_group in m.country("USA").ship_group:
|
||||
zone = [(x.position.x, x.position.y) for x in ship_group.points]
|
||||
seas_zones.append(zone)
|
||||
|
||||
with open("../{}landmap.p".format(terrain), "wb") as f:
|
||||
print(len(inclusion_zones), len(exclusion_zones))
|
||||
pickle.dump((inclusion_zones, exclusion_zones), f)
|
||||
print(len(inclusion_zones), len(exclusion_zones), len(seas_zones))
|
||||
pickle.dump((inclusion_zones, exclusion_zones, seas_zones), f)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -95,11 +95,14 @@ class ConflictTheater:
|
||||
if not self.landmap:
|
||||
return False
|
||||
|
||||
for inclusion_zone in self.landmap[0]:
|
||||
if poly_contains(point.x, point.y, inclusion_zone):
|
||||
return False
|
||||
if self.is_on_land(point):
|
||||
return False
|
||||
|
||||
return True
|
||||
for sea in self.landmap[2]:
|
||||
if poly_contains(point.x, point.y, sea):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_on_land(self, point: Point) -> bool:
|
||||
if not self.landmap:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user