mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Add option to fast forward to first contact.
This is the first step in a larger project to add play/pause buttons to the Liberation UI so the mission can be generated at any point. docs/design/turnless.md describes the plan. This adds an option to fast forward the turn to first contact before generating the mission. None of that is reflected in the UI (for now), but the miz will be generated with many flights in the air. For now "first contact" means as soon as any flight reaches its IP. I'll follow up to add threat checking so that air-to-air combat also triggers this, as will entering a SAM's threat zone. This also includes an option to halt fast-forward whenever a player flight reaches a certain mission-prep phase. This can be used to avoid fast forwarding past the player's startup time, taxi time, or takeoff time. By default this option is disabled so player aircraft may start in the air (possibly even at their IP if they're the first mission to reach IP). Fuel states do not currently account for distance traveled during fast forward. That will come later. https://github.com/dcs-liberation/dcs_liberation/issues/1681
This commit is contained in:
parent
c8f30b3289
commit
5db1b94ac4
180
doc/design/turnless.md
Normal file
180
doc/design/turnless.md
Normal file
@ -0,0 +1,180 @@
|
||||
# Turnless Campaign
|
||||
|
||||
This outlines a path for us converting Liberation from turn based into a
|
||||
turnless campaign game like BMS.
|
||||
|
||||
## Hard problems
|
||||
|
||||
### We can't simulate combat
|
||||
|
||||
We can't feasibly simulate the details of combat, nor can we generate a mission
|
||||
mid-engagement. The mission editor does not support generating aircraft
|
||||
mid-maneuver, or with any SA.
|
||||
|
||||
To avoid that problem, instead of simulating the combat we put the group in a
|
||||
"freeze" at the moment the engagement begins. Any frozen groups are spawned
|
||||
wherever they were frozen so simulation can be deferred to DCS. Each freeze will
|
||||
have a deadline for completion, at which point a simple strength-based
|
||||
autoresolve will take place.
|
||||
|
||||
After a frozen engagement is resolved (whether in sim or automatically), if the
|
||||
mission objective has not been achieved a mission abort will be evaluated.
|
||||
|
||||
### We don't know where the roads are
|
||||
|
||||
We don't know where the in-game roads are, so we can't precisely simulate the
|
||||
location of convoys as they move, nor the amount of time they will take to move
|
||||
between bases.
|
||||
|
||||
Time can be solved by just having the campaign designer encode the travel time
|
||||
in the campaign. The ME will estimate it for them so it’s simple to do. Spawn
|
||||
locations cannot be made precise, but we can have campaign designers define
|
||||
several waypoints along the route and spawn the convoy at the most appropriate
|
||||
one.
|
||||
|
||||
### SAM engagement zones are huge
|
||||
|
||||
The large threat zone of many SAMs would cause most of the map to be a frozen
|
||||
engagement.
|
||||
|
||||
To avoid that, SAMs will not cause frozen engagements, but will be simulated at
|
||||
a basic level. Each SAM will track a group, and when in range will periodically
|
||||
“shoot”, and those shots will have a chance to kill an aircraft in the targeted
|
||||
group.
|
||||
|
||||
## Systems that need to change
|
||||
|
||||
### AI Commander
|
||||
|
||||
Theater state tracking for the HTN becomes much more complicated since it needs
|
||||
to always be able to replan based on pending plans. This shouldn't be too bad
|
||||
since RndName's PR did similar and the plan to continue that should work here.
|
||||
The commander needs to learn to abort missions that are no longer viable.
|
||||
|
||||
### Transports
|
||||
|
||||
With turns gone, transports can longer move one link per turn. See the entry in
|
||||
"hard problems" above. To begin with we can pick an arbitrary time per link and
|
||||
just always spawn the transport at the origin regardless of where they ought to
|
||||
be.
|
||||
|
||||
### Turn based systems
|
||||
|
||||
A number of systems that act in a number of turns become time based:
|
||||
|
||||
- Runway repair
|
||||
- Unit delivery
|
||||
- Weather changes
|
||||
- Pilot replenishment
|
||||
- Income
|
||||
|
||||
Procurement could be made simpler if we do away with cash in favor of fixed
|
||||
replenishment rates.
|
||||
|
||||
### Front line advancement
|
||||
|
||||
Front line combat is happening continuously so we need to simulate its movement
|
||||
and losses on the front line.
|
||||
|
||||
### Mission generation
|
||||
|
||||
The mission generator needs to be able to generate aircraft in specific
|
||||
locations, with partial fuel, and possibly with some weapons expended.
|
||||
|
||||
### Mission result processing
|
||||
|
||||
Last known positions need to be recorded, as do weapon inventory and fuel
|
||||
states.
|
||||
|
||||
### UI
|
||||
|
||||
Frozen combats need to be immutable to the player.
|
||||
|
||||
Missions in progress need to be made cancelable.
|
||||
|
||||
The map needs to show the locations of in-progress missions, including frozen
|
||||
engagements.
|
||||
|
||||
There needs to be a UI to examine frozen engagements.
|
||||
|
||||
## Roadmap
|
||||
|
||||
This roadmap is ordered so that we can attempt to make this change in phases
|
||||
that may take more than a single release. It is not focused on providing the
|
||||
best gameplay experience ASAP, but on being able to approach this gradually
|
||||
rather than freezing all other development or branching and a painful merge.
|
||||
|
||||
### Front-line progress rework
|
||||
|
||||
Add auto-resolve for the front line, adjust progress estimates based on stances
|
||||
and not just kills (an enemy in retreat is less likely to be killed, but still
|
||||
gives up ground).
|
||||
|
||||
### Add play from first-contact option
|
||||
|
||||
Add simulation of the mission from the start of the turn up to the first
|
||||
engagement. Generate the mission from that point.
|
||||
|
||||
### Add fuel expenditure estimation for travel to start location
|
||||
|
||||
Adjust fuel quantities at the start of the mission to account for travel.
|
||||
|
||||
### Add UI to display simulation to first contact
|
||||
|
||||
Before removing turns, add play/pause buttons to the game that will simulate the
|
||||
mission up to the first engagement. Once play is pressed no changes may be made
|
||||
to the ATO.
|
||||
|
||||
When the turn ends it behaves like the normal end of turn. The ATO is reset and
|
||||
the new turn is planned.
|
||||
|
||||
### Add mission abort option for players
|
||||
|
||||
Add the option to abort flight and packages for players. The AI won’t use this
|
||||
yet but it will be needed once engagement simulation is added.
|
||||
|
||||
### Add simulation for frozen combats
|
||||
|
||||
Allows play to proceed through engagements. The AI will abort missions that are
|
||||
unlikely to succeed after each combat is resolved.
|
||||
|
||||
### Estimate fuel/weapon use simulation for frozen combats
|
||||
|
||||
Improve behavior for groups that have completed an auto-resolved combat by
|
||||
estimating their fuel and ammo use. This could be used to check if the mission
|
||||
should be aborted.
|
||||
|
||||
### Add player options to create new packages after pressing play
|
||||
|
||||
Allow players to create new flights as the mission simulates. This would be the
|
||||
most effective way to add intercept missions.
|
||||
|
||||
### Make planning real-time
|
||||
|
||||
Make the AI planner periodically reevaluate missions to add to the ATO. It will
|
||||
still plan as many missions as possible up front so that the DCS side of the
|
||||
mission can run as long as possible, but it could make use of aircraft that had
|
||||
aborted or returned.
|
||||
|
||||
### Time-based systems instead of turn based
|
||||
|
||||
Move the systems that are currently based on turns to be based on duration. Each
|
||||
turn is assumed to be six hours since there are four turns in a day.
|
||||
|
||||
### Time between turns based on mission duration
|
||||
|
||||
Change the turn duration to be based on the length of the mission played.
|
||||
|
||||
### Track end positions of aircraft, remove turns
|
||||
|
||||
If we can track the status of missions after the simulation ends we can continue
|
||||
where we left off. Do that, and remove the concept of turns.
|
||||
|
||||
### Track ammo/fuel status of aircraft at the end of the mission
|
||||
|
||||
Improve post-simulation realism by tracking the ammunition and fuel spent.
|
||||
|
||||
### Add turnaround times for landed aircraft
|
||||
|
||||
Once aircraft returning from a mission land, a timer starts to track the
|
||||
turnaround time before they'll be ready to redeploy as opposed to being instant.
|
||||
@ -6,6 +6,7 @@ from typing import Any, Optional, List, TYPE_CHECKING
|
||||
from gen.flights.loadouts import Loadout
|
||||
|
||||
from .flightroster import FlightRoster
|
||||
from .flightstate import Uninitialized, FlightState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
@ -52,6 +53,9 @@ class Flight:
|
||||
# Only used by transport missions.
|
||||
self.cargo = cargo
|
||||
|
||||
# Used for simulating the travel to first contact.
|
||||
self.state: FlightState = Uninitialized(self, squadron.settings)
|
||||
|
||||
# Will be replaced with a more appropriate FlightPlan by
|
||||
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
||||
# empty flight plan.
|
||||
@ -61,6 +65,19 @@ class Flight:
|
||||
package=package, flight=self, custom_waypoints=[]
|
||||
)
|
||||
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting the flight state since that's not (currently) used outside
|
||||
# mission generation. This is a bit of a hack for the moment and in the future
|
||||
# we will need to persist the flight state, but for now keep it out of save
|
||||
# compat (it also contains a generator that cannot be pickled).
|
||||
del state["state"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
state["state"] = Uninitialized(self, state["squadron"].settings)
|
||||
self.__dict__.update(state)
|
||||
|
||||
@property
|
||||
def departure(self) -> ControlPoint:
|
||||
return self.squadron.location
|
||||
@ -113,3 +130,10 @@ class Flight:
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
|
||||
def set_state(self, state: FlightState) -> None:
|
||||
self.state = state
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> bool:
|
||||
self.state.on_game_tick(time, duration)
|
||||
return self.state.should_halt_sim()
|
||||
|
||||
8
game/ato/flightstate/__init__.py
Normal file
8
game/ato/flightstate/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
from .completed import Completed
|
||||
from .flightstate import FlightState
|
||||
from .inflight import InFlight
|
||||
from .startup import StartUp
|
||||
from .takeoff import Takeoff
|
||||
from .taxi import Taxi
|
||||
from .uninitialized import Uninitialized
|
||||
from .waitingforstart import WaitingForStart
|
||||
18
game/ato/flightstate/completed.py
Normal file
18
game/ato/flightstate/completed.py
Normal file
@ -0,0 +1,18 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .flightstate import FlightState
|
||||
from ..starttype import StartType
|
||||
|
||||
|
||||
class Completed(FlightState):
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
return
|
||||
|
||||
@property
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def spawn_type(self) -> StartType:
|
||||
# TODO: May want to do something different to make these uncontrolled?
|
||||
return StartType.COLD
|
||||
34
game/ato/flightstate/flightstate.py
Normal file
34
game/ato/flightstate/flightstate.py
Normal file
@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.ato.starttype import StartType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
|
||||
|
||||
class FlightState(ABC):
|
||||
def __init__(self, flight: Flight, settings: Settings) -> None:
|
||||
self.flight = flight
|
||||
self.settings = settings
|
||||
|
||||
@abstractmethod
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
...
|
||||
|
||||
def should_halt_sim(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def spawn_type(self) -> StartType:
|
||||
...
|
||||
134
game/ato/flightstate/inflight.py
Normal file
134
game/ato/flightstate/inflight.py
Normal file
@ -0,0 +1,134 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.ato.flightstate import Completed
|
||||
from game.ato.flightstate.flightstate import FlightState
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.ato.starttype import StartType
|
||||
from game.utils import Distance, Speed, meters
|
||||
from gen.flights.flightplan import LoiterFlightPlan, PatrollingFlightPlan
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
|
||||
|
||||
def lerp(v0: float, v1: float, t: float) -> float:
|
||||
return (1 - t) * v0 + t * v1
|
||||
|
||||
|
||||
class InFlight(FlightState):
|
||||
def __init__(self, flight: Flight, settings: Settings) -> None:
|
||||
super().__init__(flight, settings)
|
||||
self.waypoints = flight.flight_plan.iter_waypoints()
|
||||
# TODO: Error checking for stupid flight plans with fewer than two waypoints.
|
||||
self.current_waypoint = next(self.waypoints)
|
||||
self.next_waypoint = next(self.waypoints)
|
||||
self.passed_waypoints = {self.current_waypoint}
|
||||
self.total_time_to_next_waypoint = self._total_time()
|
||||
self.elapsed_time = timedelta()
|
||||
|
||||
def _total_time(self) -> timedelta:
|
||||
if isinstance(self.flight.flight_plan, PatrollingFlightPlan):
|
||||
# Racetracks should remain at the first waypoint until the patrol ends so
|
||||
# that the waypoint generation doesn't need to reverse the orbit direction.
|
||||
if self.current_waypoint == self.flight.flight_plan.patrol_start:
|
||||
return self.flight.flight_plan.patrol_duration
|
||||
|
||||
# Loiter time is already built into travel_time_between_waypoints.
|
||||
return self.flight.flight_plan.travel_time_between_waypoints(
|
||||
self.current_waypoint, self.next_waypoint
|
||||
)
|
||||
|
||||
def progress(self) -> float:
|
||||
return (
|
||||
self.elapsed_time.total_seconds()
|
||||
/ self.total_time_to_next_waypoint.total_seconds()
|
||||
)
|
||||
|
||||
def estimate_position(self) -> Point:
|
||||
# TODO: Make Loiter and RaceTrack distinct FlightStates.
|
||||
if isinstance(self.flight.flight_plan, PatrollingFlightPlan):
|
||||
# Prevent spawning racetracks in the middle of a leg. For simplicity we
|
||||
# always start the aircraft at the beginning of the racetrack.
|
||||
if self.current_waypoint == self.flight.flight_plan.patrol_start:
|
||||
return self.current_waypoint.position
|
||||
elif isinstance(self.flight.flight_plan, LoiterFlightPlan):
|
||||
if (
|
||||
self.current_waypoint == self.flight.flight_plan.hold
|
||||
and self.elapsed_time < self.flight.flight_plan.hold_duration
|
||||
):
|
||||
return self.current_waypoint.position
|
||||
|
||||
x0 = self.current_waypoint.position.x
|
||||
y0 = self.current_waypoint.position.y
|
||||
x1 = self.next_waypoint.position.x
|
||||
y1 = self.next_waypoint.position.y
|
||||
progress = self.progress()
|
||||
return Point(lerp(x0, x1, progress), lerp(y0, y1, progress))
|
||||
|
||||
def estimate_altitude(self) -> tuple[Distance, str]:
|
||||
# TODO: Should count progress as 0 until departing a hold.
|
||||
return (
|
||||
meters(
|
||||
lerp(
|
||||
self.current_waypoint.alt.meters,
|
||||
self.next_waypoint.alt.meters,
|
||||
self.progress(),
|
||||
)
|
||||
),
|
||||
self.current_waypoint.alt_type,
|
||||
)
|
||||
|
||||
def estimate_speed(self) -> Speed:
|
||||
# TODO: Patrol/loiter speeds may be different.
|
||||
return self.flight.flight_plan.speed_between_waypoints(
|
||||
self.current_waypoint, self.next_waypoint
|
||||
)
|
||||
|
||||
def update_waypoints(self) -> None:
|
||||
self.current_waypoint = self.next_waypoint
|
||||
self.passed_waypoints.add(self.current_waypoint)
|
||||
try:
|
||||
self.next_waypoint = next(self.waypoints)
|
||||
except StopIteration:
|
||||
self.flight.set_state(Completed(self.flight, self.settings))
|
||||
self.total_time_to_next_waypoint = self._total_time()
|
||||
self.elapsed_time = timedelta()
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
self.elapsed_time += duration
|
||||
if self.elapsed_time > self.total_time_to_next_waypoint:
|
||||
self.update_waypoints()
|
||||
|
||||
def should_halt_sim(self) -> bool:
|
||||
contact_types = {
|
||||
FlightWaypointType.INGRESS_BAI,
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_DEAD,
|
||||
FlightWaypointType.INGRESS_OCA_AIRCRAFT,
|
||||
FlightWaypointType.INGRESS_OCA_RUNWAY,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
# TODO: Check against enemy threats.
|
||||
if self.current_waypoint.waypoint_type in contact_types:
|
||||
logging.info(
|
||||
f"Interrupting simulation because {self.flight} has reached its "
|
||||
"ingress point"
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def spawn_type(self) -> StartType:
|
||||
return StartType.IN_FLIGHT
|
||||
44
game/ato/flightstate/startup.py
Normal file
44
game/ato/flightstate/startup.py
Normal file
@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .flightstate import FlightState
|
||||
from .taxi import Taxi
|
||||
from ..starttype import StartType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
|
||||
|
||||
class StartUp(FlightState):
|
||||
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
|
||||
super().__init__(flight, settings)
|
||||
self.completion_time = now + flight.flight_plan.estimate_startup()
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
if time < self.completion_time:
|
||||
return
|
||||
self.flight.set_state(Taxi(self.flight, self.settings, time))
|
||||
|
||||
@property
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def spawn_type(self) -> StartType:
|
||||
return StartType.COLD
|
||||
|
||||
def should_halt_sim(self) -> bool:
|
||||
if (
|
||||
self.flight.client_count > 0
|
||||
and self.settings.player_mission_interrupts_sim_at is StartType.COLD
|
||||
):
|
||||
logging.info(
|
||||
f"Interrupting simulation because {self.flight} has players and has "
|
||||
"reached startup time"
|
||||
)
|
||||
return True
|
||||
return False
|
||||
45
game/ato/flightstate/takeoff.py
Normal file
45
game/ato/flightstate/takeoff.py
Normal file
@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .flightstate import FlightState
|
||||
from .inflight import InFlight
|
||||
from ..starttype import StartType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
|
||||
|
||||
class Takeoff(FlightState):
|
||||
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
|
||||
super().__init__(flight, settings)
|
||||
# TODO: Not accounted for in FlightPlan, can cause discrepancy without loiter.
|
||||
self.completion_time = now + timedelta(seconds=30)
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
if time < self.completion_time:
|
||||
return
|
||||
self.flight.set_state(InFlight(self.flight, self.settings))
|
||||
|
||||
@property
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def spawn_type(self) -> StartType:
|
||||
return StartType.RUNWAY
|
||||
|
||||
def should_halt_sim(self) -> bool:
|
||||
if (
|
||||
self.flight.client_count > 0
|
||||
and self.settings.player_mission_interrupts_sim_at is StartType.RUNWAY
|
||||
):
|
||||
logging.info(
|
||||
f"Interrupting simulation because {self.flight} has players and has "
|
||||
"reached takeoff time"
|
||||
)
|
||||
return True
|
||||
return False
|
||||
44
game/ato/flightstate/taxi.py
Normal file
44
game/ato/flightstate/taxi.py
Normal file
@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .flightstate import FlightState
|
||||
from .takeoff import Takeoff
|
||||
from ..starttype import StartType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
|
||||
|
||||
class Taxi(FlightState):
|
||||
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
|
||||
super().__init__(flight, settings)
|
||||
self.completion_time = now + flight.flight_plan.estimate_ground_ops()
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
if time < self.completion_time:
|
||||
return
|
||||
self.flight.set_state(Takeoff(self.flight, self.settings, time))
|
||||
|
||||
@property
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def spawn_type(self) -> StartType:
|
||||
return StartType.WARM
|
||||
|
||||
def should_halt_sim(self) -> bool:
|
||||
if (
|
||||
self.flight.client_count > 0
|
||||
and self.settings.player_mission_interrupts_sim_at is StartType.WARM
|
||||
):
|
||||
logging.info(
|
||||
f"Interrupting simulation because {self.flight} has players and has "
|
||||
"reached taxi time"
|
||||
)
|
||||
return True
|
||||
return False
|
||||
17
game/ato/flightstate/uninitialized.py
Normal file
17
game/ato/flightstate/uninitialized.py
Normal file
@ -0,0 +1,17 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .flightstate import FlightState
|
||||
from ..starttype import StartType
|
||||
|
||||
|
||||
class Uninitialized(FlightState):
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||
|
||||
@property
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||
|
||||
@property
|
||||
def spawn_type(self) -> StartType:
|
||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||
56
game/ato/flightstate/waitingforstart.py
Normal file
56
game/ato/flightstate/waitingforstart.py
Normal file
@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.ato.starttype import StartType
|
||||
from .flightstate import FlightState
|
||||
from .inflight import InFlight
|
||||
from .startup import StartUp
|
||||
from .takeoff import Takeoff
|
||||
from .taxi import Taxi
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.settings import Settings
|
||||
|
||||
|
||||
class WaitingForStart(FlightState):
|
||||
def __init__(
|
||||
self,
|
||||
flight: Flight,
|
||||
settings: Settings,
|
||||
start_time: datetime,
|
||||
) -> None:
|
||||
super().__init__(flight, settings)
|
||||
self.start_time = start_time
|
||||
|
||||
@property
|
||||
def start_type(self) -> StartType:
|
||||
return self.flight.start_type
|
||||
|
||||
def on_game_tick(self, time: datetime, duration: timedelta) -> None:
|
||||
if time < self.start_time:
|
||||
return
|
||||
|
||||
new_state: FlightState
|
||||
if self.start_type is StartType.COLD:
|
||||
new_state = StartUp(self.flight, self.settings, time)
|
||||
elif self.start_type is StartType.WARM:
|
||||
new_state = Taxi(self.flight, self.settings, time)
|
||||
elif self.start_type is StartType.RUNWAY:
|
||||
new_state = Takeoff(self.flight, self.settings, time)
|
||||
else:
|
||||
new_state = InFlight(self.flight, self.settings)
|
||||
self.flight.set_state(new_state)
|
||||
|
||||
@property
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
return True
|
||||
|
||||
def time_remaining(self, time: datetime) -> timedelta:
|
||||
return self.start_time - time
|
||||
|
||||
@property
|
||||
def spawn_type(self) -> StartType:
|
||||
return self.flight.start_type
|
||||
@ -63,34 +63,3 @@ class FlightWaypoint:
|
||||
@property
|
||||
def position(self) -> Point:
|
||||
return Point(self.x, self.y)
|
||||
|
||||
@classmethod
|
||||
def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint":
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.NAV,
|
||||
point.position.x,
|
||||
point.position.y,
|
||||
meters(point.alt),
|
||||
)
|
||||
waypoint.alt_type = point.alt_type
|
||||
# Other actions exist... but none of them *should* be the first
|
||||
# waypoint for a flight.
|
||||
waypoint.waypoint_type = {
|
||||
PointAction.TurningPoint: FlightWaypointType.NAV,
|
||||
PointAction.FlyOverPoint: FlightWaypointType.NAV,
|
||||
PointAction.FromParkingArea: FlightWaypointType.TAKEOFF,
|
||||
PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF,
|
||||
PointAction.FromRunway: FlightWaypointType.TAKEOFF,
|
||||
PointAction.FromGroundArea: FlightWaypointType.TAKEOFF,
|
||||
PointAction.FromGroundAreaHot: FlightWaypointType.TAKEOFF,
|
||||
}[point.action]
|
||||
if waypoint.waypoint_type == FlightWaypointType.NAV:
|
||||
waypoint.name = "NAV"
|
||||
waypoint.pretty_name = "Nav"
|
||||
waypoint.description = "Nav"
|
||||
else:
|
||||
waypoint.name = "TAKEOFF"
|
||||
waypoint.pretty_name = "Takeoff"
|
||||
waypoint.description = "Takeoff"
|
||||
waypoint.description = f"Takeoff from {from_cp.name}"
|
||||
return waypoint
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from dcs.task import (
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from functools import cached_property
|
||||
from typing import Any, Dict, List, TYPE_CHECKING
|
||||
|
||||
@ -11,6 +12,7 @@ from dcs.unitgroup import FlyingGroup, StaticGroup
|
||||
|
||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.flightstate import Completed
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.package import Package
|
||||
from game.ato.starttype import StartType
|
||||
@ -42,6 +44,7 @@ class AircraftGenerator:
|
||||
mission: Mission,
|
||||
settings: Settings,
|
||||
game: Game,
|
||||
time: datetime,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
laser_code_registry: LaserCodeRegistry,
|
||||
@ -49,9 +52,10 @@ class AircraftGenerator:
|
||||
air_support: AirSupport,
|
||||
helipads: dict[ControlPoint, list[StaticGroup]],
|
||||
) -> None:
|
||||
self.m = mission
|
||||
self.game = game
|
||||
self.mission = mission
|
||||
self.settings = settings
|
||||
self.game = game
|
||||
self.time = time
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registy = tacan_registry
|
||||
self.laser_code_registry = laser_code_registry
|
||||
@ -144,9 +148,10 @@ class AircraftGenerator:
|
||||
StartType.COLD,
|
||||
divert=None,
|
||||
)
|
||||
flight.state = Completed(flight, self.game.settings)
|
||||
|
||||
group = FlightGroupSpawner(
|
||||
flight, country, self.m, self.helipads
|
||||
flight, country, self.mission, self.helipads
|
||||
).create_idle_aircraft()
|
||||
AircraftPainter(flight, group).apply_livery()
|
||||
self.unit_map.add_aircraft(group, flight)
|
||||
@ -154,15 +159,17 @@ class AircraftGenerator:
|
||||
def create_and_configure_flight(
|
||||
self, flight: Flight, country: Country, dynamic_runways: Dict[str, RunwayData]
|
||||
) -> FlyingGroup[Any]:
|
||||
"""Creates and configures the flight group in the mission."""
|
||||
group = FlightGroupSpawner(
|
||||
flight, country, self.m, self.helipads
|
||||
flight, country, self.mission, self.helipads
|
||||
).create_flight_group()
|
||||
self.flights.append(
|
||||
FlightGroupConfigurator(
|
||||
flight,
|
||||
group,
|
||||
self.game,
|
||||
self.m,
|
||||
self.mission,
|
||||
self.time,
|
||||
self.radio_registry,
|
||||
self.tacan_registy,
|
||||
self.laser_code_registry,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs import Mission
|
||||
@ -36,6 +37,7 @@ class FlightGroupConfigurator:
|
||||
group: FlyingGroup[Any],
|
||||
game: Game,
|
||||
mission: Mission,
|
||||
time: datetime,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
laser_code_registry: LaserCodeRegistry,
|
||||
@ -47,6 +49,7 @@ class FlightGroupConfigurator:
|
||||
self.group = group
|
||||
self.game = game
|
||||
self.mission = mission
|
||||
self.time = time
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
self.laser_code_registry = laser_code_registry
|
||||
@ -72,7 +75,13 @@ class FlightGroupConfigurator:
|
||||
)
|
||||
|
||||
mission_start_time, waypoints = WaypointGenerator(
|
||||
self.flight, self.group, self.mission, self.game.settings, self.air_support
|
||||
self.flight,
|
||||
self.group,
|
||||
self.mission,
|
||||
self.game.conditions.start_time,
|
||||
self.time,
|
||||
self.game.settings,
|
||||
self.air_support,
|
||||
).create_waypoints()
|
||||
|
||||
return FlightData(
|
||||
|
||||
@ -12,6 +12,7 @@ from dcs.terrain import Airport, NoParkingSlotError
|
||||
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||
|
||||
from game.ato import Flight
|
||||
from game.ato.flightstate import InFlight
|
||||
from game.ato.starttype import StartType
|
||||
from game.theater import Airfield, ControlPoint, NavalControlPoint, OffMapSpawn
|
||||
from game.utils import meters
|
||||
@ -40,7 +41,31 @@ class FlightGroupSpawner:
|
||||
self.helipads = helipads
|
||||
|
||||
def create_flight_group(self) -> FlyingGroup[Any]:
|
||||
return self.generate_flight_at_departure()
|
||||
"""Creates the group for the flight and adds it to the mission.
|
||||
|
||||
Each flight is spawned according to its FlightState at the time of mission
|
||||
generation. Aircraft that are WaitingForStart will be set up based on their
|
||||
StartType with a delay. Note that delays are actually created during waypoint
|
||||
generation.
|
||||
|
||||
Aircraft that are *not* WaitingForStart will be spawned in their current state.
|
||||
We cannot spawn aircraft mid-taxi, so when the simulated state is near the end
|
||||
of a long taxi period the aircraft will be spawned in their parking spot. This
|
||||
could lead to problems but that's what loiter points are for. The other pre-
|
||||
flight states have the same problem but are much shorter and more easily covered
|
||||
by the loiter time. Player flights that are spawned near the end of their cold
|
||||
start have the biggest problem but players are able to cut corners to make up
|
||||
for lost time.
|
||||
|
||||
Aircraft that are already in the air will be spawned at their estimated
|
||||
location, speed, and altitude based on their flight plan.
|
||||
"""
|
||||
if (
|
||||
self.flight.state.is_waiting_for_start
|
||||
or self.flight.state.spawn_type is not StartType.IN_FLIGHT
|
||||
):
|
||||
return self.generate_flight_at_departure()
|
||||
return self.generate_mid_mission()
|
||||
|
||||
def create_idle_aircraft(self) -> FlyingGroup[Any]:
|
||||
assert isinstance(self.flight.squadron.location, Airfield)
|
||||
@ -54,13 +79,17 @@ class FlightGroupSpawner:
|
||||
group.uncontrolled = True
|
||||
return group
|
||||
|
||||
@property
|
||||
def start_type(self) -> StartType:
|
||||
return self.flight.state.spawn_type
|
||||
|
||||
def generate_flight_at_departure(self) -> FlyingGroup[Any]:
|
||||
name = namegen.next_aircraft_name(
|
||||
self.country, self.flight.departure.id, self.flight
|
||||
)
|
||||
cp = self.flight.departure
|
||||
try:
|
||||
if self.flight.start_type is StartType.IN_FLIGHT:
|
||||
if self.start_type is StartType.IN_FLIGHT:
|
||||
group = self._generate_over_departure(name, cp)
|
||||
return group
|
||||
elif isinstance(cp, NavalControlPoint):
|
||||
@ -69,7 +98,7 @@ class FlightGroupSpawner:
|
||||
if not isinstance(carrier_group, ShipGroup):
|
||||
raise RuntimeError(
|
||||
f"Carrier group {carrier_group} is a "
|
||||
"{carrier_group.__class__.__name__}, expected a ShipGroup"
|
||||
f"{carrier_group.__class__.__name__}, expected a ShipGroup"
|
||||
)
|
||||
return self._generate_at_group(name, carrier_group)
|
||||
else:
|
||||
@ -88,11 +117,33 @@ class FlightGroupSpawner:
|
||||
logging.exception(
|
||||
"No room on runway or parking slots. Starting from the air."
|
||||
)
|
||||
self.flight.start_type = StartType.IN_FLIGHT
|
||||
group = self._generate_over_departure(name, cp)
|
||||
group.points[0].alt = 1500
|
||||
return group
|
||||
|
||||
def generate_mid_mission(self) -> FlyingGroup[Any]:
|
||||
assert isinstance(self.flight.state, InFlight)
|
||||
name = namegen.next_aircraft_name(
|
||||
self.country, self.flight.departure.id, self.flight
|
||||
)
|
||||
speed = self.flight.state.estimate_speed()
|
||||
pos = self.flight.state.estimate_position()
|
||||
alt, alt_type = self.flight.state.estimate_altitude()
|
||||
group = self.mission.flight_group(
|
||||
country=self.country,
|
||||
name=name,
|
||||
aircraft_type=self.flight.unit_type.dcs_unit_type,
|
||||
airport=None,
|
||||
position=pos,
|
||||
altitude=alt.meters,
|
||||
speed=speed.kph,
|
||||
maintask=None,
|
||||
group_size=self.flight.count,
|
||||
)
|
||||
|
||||
group.points[0].alt_type = alt_type
|
||||
return group
|
||||
|
||||
def _generate_at_airport(self, name: str, airport: Airport) -> FlyingGroup[Any]:
|
||||
# TODO: Delayed runway starts should be converted to air starts for multiplayer.
|
||||
# Runway starts do not work with late activated aircraft in multiplayer. Instead
|
||||
@ -169,7 +220,7 @@ class FlightGroupSpawner:
|
||||
group.points[0].action = PointAction.FromGroundArea
|
||||
group.points[0].type = "TakeOffGround"
|
||||
group.units[0].heading = helipad.units[0].heading
|
||||
if self.flight.start_type != "Cold":
|
||||
if self.start_type is not StartType.COLD:
|
||||
group.points[0].action = PointAction.FromGroundAreaHot
|
||||
group.points[0].type = "TakeOffGroundHot"
|
||||
|
||||
@ -183,15 +234,13 @@ class FlightGroupSpawner:
|
||||
return group
|
||||
|
||||
def dcs_start_type(self) -> DcsStartType:
|
||||
if self.flight.start_type is StartType.RUNWAY:
|
||||
if self.start_type is StartType.RUNWAY:
|
||||
return DcsStartType.Runway
|
||||
elif self.flight.start_type is StartType.COLD:
|
||||
elif self.start_type is StartType.COLD:
|
||||
return DcsStartType.Cold
|
||||
elif self.flight.start_type is StartType.WARM:
|
||||
elif self.start_type is StartType.WARM:
|
||||
return DcsStartType.Warm
|
||||
raise ValueError(
|
||||
f"There is no pydcs StartType matching {self.flight.start_type}"
|
||||
)
|
||||
raise ValueError(f"There is no pydcs StartType matching {self.start_type}")
|
||||
|
||||
def _start_type_at_group(
|
||||
self,
|
||||
|
||||
@ -9,9 +9,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class BaiIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
# TODO: Add common "UnitGroupTarget" base type.
|
||||
group_names = []
|
||||
target = self.package.target
|
||||
@ -25,7 +23,7 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
|
||||
"Unexpected target type for BAI mission: %s",
|
||||
target.__class__.__name__,
|
||||
)
|
||||
return waypoint
|
||||
return
|
||||
|
||||
for group_name in group_names:
|
||||
group = self.mission.find_group(group_name)
|
||||
@ -39,4 +37,3 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
return waypoint
|
||||
|
||||
@ -9,8 +9,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class CasIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
||||
waypoint.add_task(
|
||||
EngageTargetsInZone(
|
||||
@ -35,4 +34,3 @@ class CasIngressBuilder(PydcsWaypointBuilder):
|
||||
],
|
||||
)
|
||||
)
|
||||
return waypoint
|
||||
|
||||
@ -8,8 +8,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
self.register_special_waypoints(self.waypoint.targets)
|
||||
|
||||
target = self.package.target
|
||||
@ -18,7 +17,7 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||
"Unexpected target type for DEAD mission: %s",
|
||||
target.__class__.__name__,
|
||||
)
|
||||
return waypoint
|
||||
return
|
||||
|
||||
for group in target.groups:
|
||||
miz_group = self.mission.find_group(group.name)
|
||||
@ -33,4 +32,3 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
return waypoint
|
||||
|
||||
@ -8,8 +8,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
loiter = ControlledTask(
|
||||
OrbitAction(altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.Circle)
|
||||
)
|
||||
@ -20,9 +19,10 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
f"{flight_plan_type} does not define a push time. AI will push "
|
||||
"immediately and may flight unsuitable speeds."
|
||||
)
|
||||
return waypoint
|
||||
return
|
||||
push_time = self.flight.flight_plan.push_time
|
||||
self.waypoint.departure_time = push_time
|
||||
loiter.stop_after_time(int(push_time.total_seconds()))
|
||||
loiter.stop_after_time(
|
||||
int((push_time - self.elapsed_mission_time).total_seconds())
|
||||
)
|
||||
waypoint.add_task(loiter)
|
||||
return waypoint
|
||||
|
||||
@ -9,8 +9,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
if self.flight.flight_type == FlightType.ESCORT:
|
||||
self.configure_escort_tasks(
|
||||
waypoint,
|
||||
@ -23,7 +22,6 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
self.configure_escort_tasks(
|
||||
waypoint, [Targets.All.GroundUnits.AirDefence.AAA.SAMRelated]
|
||||
)
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def configure_escort_tasks(
|
||||
|
||||
@ -9,16 +9,14 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class OcaAircraftIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
target = self.package.target
|
||||
if not isinstance(target, Airfield):
|
||||
logging.error(
|
||||
"Unexpected target type for OCA Strike mission: %s",
|
||||
target.__class__.__name__,
|
||||
)
|
||||
return waypoint
|
||||
return
|
||||
|
||||
task = EngageTargetsInZone(
|
||||
position=target.position,
|
||||
@ -32,4 +30,3 @@ class OcaAircraftIngressBuilder(PydcsWaypointBuilder):
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
return waypoint
|
||||
|
||||
@ -8,18 +8,15 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class OcaRunwayIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
target = self.package.target
|
||||
if not isinstance(target, Airfield):
|
||||
logging.error(
|
||||
"Unexpected target type for runway bombing mission: %s",
|
||||
target.__class__.__name__,
|
||||
)
|
||||
return waypoint
|
||||
return
|
||||
|
||||
waypoint.tasks.append(
|
||||
BombingRunway(airport_id=target.airport.id, group_attack=True)
|
||||
)
|
||||
return waypoint
|
||||
|
||||
@ -28,6 +28,7 @@ class PydcsWaypointBuilder:
|
||||
group: FlyingGroup[Any],
|
||||
flight: Flight,
|
||||
mission: Mission,
|
||||
elapsed_mission_time: timedelta,
|
||||
air_support: AirSupport,
|
||||
) -> None:
|
||||
self.waypoint = waypoint
|
||||
@ -35,6 +36,7 @@ class PydcsWaypointBuilder:
|
||||
self.package = flight.package
|
||||
self.flight = flight
|
||||
self.mission = mission
|
||||
self.elapsed_mission_time = elapsed_mission_time
|
||||
self.air_support = air_support
|
||||
|
||||
def build(self) -> MovingPoint:
|
||||
@ -54,12 +56,16 @@ class PydcsWaypointBuilder:
|
||||
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
|
||||
if tot is not None:
|
||||
self.set_waypoint_tot(waypoint, tot)
|
||||
self.add_tasks(waypoint)
|
||||
return waypoint
|
||||
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
pass
|
||||
|
||||
def set_waypoint_tot(self, waypoint: MovingPoint, tot: timedelta) -> None:
|
||||
self.waypoint.tot = tot
|
||||
if not self._viggen_client_tot():
|
||||
waypoint.ETA = int(tot.total_seconds())
|
||||
waypoint.ETA = int((tot - self.elapsed_mission_time).total_seconds())
|
||||
waypoint.ETA_locked = True
|
||||
waypoint.speed_locked = False
|
||||
|
||||
|
||||
@ -16,9 +16,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
flight_plan = self.flight.flight_plan
|
||||
|
||||
if not isinstance(flight_plan, PatrollingFlightPlan):
|
||||
@ -27,7 +25,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
f"Cannot create race track for {self.flight} because "
|
||||
f"{flight_plan_type} does not define a patrol."
|
||||
)
|
||||
return waypoint
|
||||
return
|
||||
|
||||
# NB: It's important that the engage task comes before the orbit task.
|
||||
# Though they're on the same waypoint, if the orbit task comes first it
|
||||
@ -58,11 +56,10 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
|
||||
racetrack = ControlledTask(orbit)
|
||||
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
||||
racetrack.stop_after_time(int(flight_plan.patrol_end_time.total_seconds()))
|
||||
loiter_duration = flight_plan.patrol_end_time - self.elapsed_mission_time
|
||||
racetrack.stop_after_time(int(loiter_duration.total_seconds()))
|
||||
waypoint.add_task(racetrack)
|
||||
|
||||
return waypoint
|
||||
|
||||
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
|
||||
waypoint.add_task(Tanker())
|
||||
|
||||
|
||||
@ -8,8 +8,7 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
self.register_special_waypoints(self.waypoint.targets)
|
||||
|
||||
target = self.package.target
|
||||
@ -18,7 +17,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||
"Unexpected target type for SEAD mission: %s",
|
||||
target.__class__.__name__,
|
||||
)
|
||||
return waypoint
|
||||
return
|
||||
|
||||
for group in target.groups:
|
||||
miz_group = self.mission.find_group(group.name)
|
||||
@ -33,4 +32,3 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
return waypoint
|
||||
|
||||
@ -7,18 +7,16 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
if self.group.units[0].unit_type in [B_17G, B_52H, Tu_22M3]:
|
||||
return self.build_bombing()
|
||||
self.add_bombing_tasks(waypoint)
|
||||
else:
|
||||
return self.build_strike()
|
||||
|
||||
def build_bombing(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
self.add_strike_tasks(waypoint)
|
||||
|
||||
def add_bombing_tasks(self, waypoint: MovingPoint) -> None:
|
||||
targets = self.waypoint.targets
|
||||
if not targets:
|
||||
return waypoint
|
||||
return
|
||||
|
||||
center = Point(0, 0)
|
||||
for target in targets:
|
||||
@ -33,10 +31,8 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||
bombing.params["altitudeEnabled"] = False
|
||||
bombing.params["groupAttack"] = True
|
||||
waypoint.tasks.append(bombing)
|
||||
return waypoint
|
||||
|
||||
def build_strike(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
def add_strike_tasks(self, waypoint: MovingPoint) -> None:
|
||||
for target in self.waypoint.targets:
|
||||
bombing = Bombing(target.position, weapon_type=WeaponType.Auto)
|
||||
# If there is only one target, drop all ordnance in one pass.
|
||||
@ -47,4 +43,3 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||
|
||||
# Register special waypoints
|
||||
self.register_special_waypoints(self.waypoint.targets)
|
||||
return waypoint
|
||||
|
||||
@ -9,16 +9,14 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class SweepIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
if not isinstance(self.flight.flight_plan, SweepFlightPlan):
|
||||
flight_plan_type = self.flight.flight_plan.__class__.__name__
|
||||
logging.error(
|
||||
f"Cannot create sweep for {self.flight} because "
|
||||
f"{flight_plan_type} is not a sweep flight plan."
|
||||
)
|
||||
return waypoint
|
||||
return
|
||||
|
||||
waypoint.tasks.append(
|
||||
EngageTargets(
|
||||
@ -29,5 +27,3 @@ class SweepIngressBuilder(PydcsWaypointBuilder):
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
return waypoint
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import itertools
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from dcs import Mission
|
||||
@ -12,15 +12,13 @@ from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
from game.ato import Flight, FlightWaypoint
|
||||
from game.ato.flightstate import InFlight, WaitingForStart
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.ato.starttype import StartType
|
||||
from game.missiongenerator.airsupport import AirSupport
|
||||
from game.settings import Settings
|
||||
from game.theater import ControlPointType
|
||||
from game.utils import meters, pairwise
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
|
||||
from .baiingress import BaiIngressBuilder
|
||||
from .cargostop import CargoStopBuilder
|
||||
from .casingress import CasIngressBuilder
|
||||
@ -31,6 +29,7 @@ from .joinpoint import JoinPointBuilder
|
||||
from .landingpoint import LandingPointBuilder
|
||||
from .ocaaircraftingress import OcaAircraftIngressBuilder
|
||||
from .ocarunwayingress import OcaRunwayIngressBuilder
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
|
||||
from .racetrack import RaceTrackBuilder
|
||||
from .racetrackend import RaceTrackEndBuilder
|
||||
from .seadingress import SeadIngressBuilder
|
||||
@ -44,12 +43,16 @@ class WaypointGenerator:
|
||||
flight: Flight,
|
||||
group: FlyingGroup[Any],
|
||||
mission: Mission,
|
||||
turn_start_time: datetime,
|
||||
time: datetime,
|
||||
settings: Settings,
|
||||
air_support: AirSupport,
|
||||
) -> None:
|
||||
self.flight = flight
|
||||
self.group = group
|
||||
self.mission = mission
|
||||
self.elapsed_mission_time = time - turn_start_time
|
||||
self.time = time
|
||||
self.settings = settings
|
||||
self.air_support = air_support
|
||||
|
||||
@ -57,17 +60,22 @@ class WaypointGenerator:
|
||||
for waypoint in self.flight.points:
|
||||
waypoint.tot = None
|
||||
|
||||
takeoff_point = FlightWaypoint.from_pydcs(
|
||||
self.group.points[0], self.flight.from_cp
|
||||
)
|
||||
mission_start_time = self.set_takeoff_time(takeoff_point)
|
||||
waypoints = self.flight.flight_plan.waypoints
|
||||
mission_start_time = self.set_takeoff_time(waypoints[0])
|
||||
|
||||
filtered_points: list[FlightWaypoint] = []
|
||||
|
||||
for point in self.flight.points:
|
||||
if point.only_for_player and not self.flight.client_count:
|
||||
continue
|
||||
filtered_points.append(point)
|
||||
if isinstance(self.flight.state, InFlight):
|
||||
if point == self.flight.state.current_waypoint:
|
||||
# We don't need to build this waypoint because pydcs did that for
|
||||
# us, but we do need to configure the tasks for it so that mid-
|
||||
# mission aircraft starting at a waypoint with tasks behave
|
||||
# correctly.
|
||||
self.builder_for_waypoint(point).add_tasks(self.group.points[0])
|
||||
if point not in self.flight.state.passed_waypoints:
|
||||
filtered_points.append(point)
|
||||
# Only add 1 target waypoint for Viggens. This only affects player flights, the
|
||||
# Viggen can't have more than 9 waypoints which leaves us with two target point
|
||||
# under the current flight plans.
|
||||
@ -100,7 +108,6 @@ class WaypointGenerator:
|
||||
# estimation ability the minimum fuel amounts will be calculated during flight
|
||||
# plan construction, but for now it's only used by the kneeboard so is generated
|
||||
# late.
|
||||
waypoints = [takeoff_point] + self.flight.points
|
||||
self._estimate_min_fuel_for(waypoints)
|
||||
return mission_start_time, waypoints
|
||||
|
||||
@ -124,7 +131,12 @@ class WaypointGenerator:
|
||||
}
|
||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||
return builder(
|
||||
waypoint, self.group, self.flight, self.mission, self.air_support
|
||||
waypoint,
|
||||
self.group,
|
||||
self.flight,
|
||||
self.mission,
|
||||
self.elapsed_mission_time,
|
||||
self.air_support,
|
||||
)
|
||||
|
||||
def _estimate_min_fuel_for(self, waypoints: list[FlightWaypoint]) -> None:
|
||||
@ -171,23 +183,25 @@ class WaypointGenerator:
|
||||
a.min_fuel = min_fuel
|
||||
|
||||
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:
|
||||
estimator = TotEstimator(self.flight.package)
|
||||
start_time = estimator.mission_start_time(self.flight)
|
||||
if isinstance(self.flight.state, WaitingForStart):
|
||||
delay = self.flight.state.time_remaining(self.time)
|
||||
else:
|
||||
delay = timedelta()
|
||||
|
||||
if self.should_delay_flight(start_time):
|
||||
if self.should_delay_flight():
|
||||
if self.should_activate_late():
|
||||
# Late activation causes the aircraft to not be spawned
|
||||
# until triggered.
|
||||
self.set_activation_time(start_time)
|
||||
self.set_activation_time(delay)
|
||||
elif self.flight.start_type is StartType.COLD:
|
||||
# Setting the start time causes the AI to wait until the
|
||||
# specified time to begin their startup sequence.
|
||||
self.set_startup_time(start_time)
|
||||
self.set_startup_time(delay)
|
||||
|
||||
# And setting *our* waypoint TOT causes the takeoff time to show up in
|
||||
# the player's kneeboard.
|
||||
waypoint.tot = self.flight.flight_plan.takeoff_time()
|
||||
return start_time
|
||||
return delay
|
||||
|
||||
def set_activation_time(self, delay: timedelta) -> None:
|
||||
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
||||
@ -232,17 +246,17 @@ class WaypointGenerator:
|
||||
activation_trigger.add_action(AITaskPush(self.group.id, len(self.group.tasks)))
|
||||
self.mission.triggerrules.triggers.append(activation_trigger)
|
||||
|
||||
def should_delay_flight(self, start_time: timedelta) -> bool:
|
||||
if start_time.total_seconds() <= 0:
|
||||
def should_delay_flight(self) -> bool:
|
||||
if not isinstance(self.flight.state, WaitingForStart):
|
||||
return False
|
||||
|
||||
if not self.flight.client_count:
|
||||
return True
|
||||
|
||||
if start_time < timedelta(minutes=10):
|
||||
# Don't bother delaying client flights with short start delays. Much
|
||||
# more than ten minutes starts to eat into fuel a bit more
|
||||
# (espeicially for something fuel limited like a Harrier).
|
||||
if self.flight.state.time_remaining(self.time) < timedelta(minutes=10):
|
||||
# Don't bother delaying client flights with short start delays. Much more
|
||||
# than ten minutes starts to eat into fuel a bit more (especially for
|
||||
# something fuel limited like a Harrier).
|
||||
return False
|
||||
|
||||
return not self.settings.never_delay_player_flights
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from dcs.mission import Mission
|
||||
@ -6,9 +7,12 @@ from game.weather import Clouds, Fog, Conditions, WindConditions, AtmosphericCon
|
||||
|
||||
|
||||
class EnvironmentGenerator:
|
||||
def __init__(self, mission: Mission, conditions: Conditions) -> None:
|
||||
def __init__(
|
||||
self, mission: Mission, conditions: Conditions, time: datetime
|
||||
) -> None:
|
||||
self.mission = mission
|
||||
self.conditions = conditions
|
||||
self.time = time
|
||||
|
||||
def set_atmospheric(self, atmospheric: AtmosphericConditions) -> None:
|
||||
self.mission.weather.qnh = atmospheric.qnh.mm_hg
|
||||
@ -35,7 +39,7 @@ class EnvironmentGenerator:
|
||||
self.mission.weather.wind_at_8000 = wind.at_8000m
|
||||
|
||||
def generate(self) -> None:
|
||||
self.mission.start_time = self.conditions.start_time
|
||||
self.mission.start_time = self.time
|
||||
self.set_atmospheric(self.conditions.weather.atmospheric)
|
||||
self.set_clouds(self.conditions.weather.clouds)
|
||||
self.set_fog(self.conditions.weather.fog)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
@ -46,8 +47,9 @@ COMBINED_ARMS_SLOTS = 1
|
||||
|
||||
|
||||
class MissionGenerator:
|
||||
def __init__(self, game: Game) -> None:
|
||||
def __init__(self, game: Game, time: datetime) -> None:
|
||||
self.game = game
|
||||
self.time = time
|
||||
self.mission = Mission(game.theater.terrain)
|
||||
self.unit_map = UnitMap()
|
||||
|
||||
@ -74,7 +76,7 @@ class MissionGenerator:
|
||||
self.add_airfields_to_unit_map()
|
||||
self.initialize_registries()
|
||||
|
||||
EnvironmentGenerator(self.mission, self.game.conditions).generate()
|
||||
EnvironmentGenerator(self.mission, self.game.conditions, self.time).generate()
|
||||
|
||||
tgo_generator = TgoGenerator(
|
||||
self.mission,
|
||||
@ -232,6 +234,7 @@ class MissionGenerator:
|
||||
self.mission,
|
||||
self.game.settings,
|
||||
self.game,
|
||||
self.time,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.laser_code_registry,
|
||||
|
||||
@ -286,6 +286,34 @@ class Settings:
|
||||
|
||||
# Mission Generator
|
||||
# Gameplay
|
||||
fast_forward_to_first_contact: bool = boolean_option(
|
||||
"Fast forward mission to first contact (WIP)",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=False,
|
||||
detail=(
|
||||
"If enabled, the mission will be generated at the point of first contact."
|
||||
),
|
||||
)
|
||||
player_mission_interrupts_sim_at: Optional[StartType] = choices_option(
|
||||
"Player missions interrupt fast forward",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=None,
|
||||
choices={
|
||||
"Never": None,
|
||||
"At startup time": StartType.COLD,
|
||||
"At taxi time": StartType.WARM,
|
||||
"At takeoff time": StartType.RUNWAY,
|
||||
},
|
||||
detail=(
|
||||
"Determines what player mission states will interrupt fast-forwarding to "
|
||||
"first contact, if enabled. If never is selected player missions will not "
|
||||
"impact simulation and player missions may be generated mid-flight. The "
|
||||
"other options will cause the mission to be generated as soon as a player "
|
||||
"mission reaches the set state or at first contact, whichever comes first."
|
||||
),
|
||||
)
|
||||
supercarrier: bool = boolean_option(
|
||||
"Use supercarrier module",
|
||||
MISSION_GENERATOR_PAGE,
|
||||
|
||||
94
game/sim/aircraftsimulation.py
Normal file
94
game/sim/aircraftsimulation.py
Normal file
@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from collections import Iterator
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from typing_extensions import TYPE_CHECKING
|
||||
|
||||
from game.ato import Flight
|
||||
from game.ato.flightstate import (
|
||||
InFlight,
|
||||
StartUp,
|
||||
Takeoff,
|
||||
Taxi,
|
||||
Uninitialized,
|
||||
WaitingForStart,
|
||||
)
|
||||
from game.ato.starttype import StartType
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
TICK = timedelta(seconds=1)
|
||||
|
||||
|
||||
class AircraftSimulation:
|
||||
def __init__(self, game: Game) -> None:
|
||||
self.game = game
|
||||
self.time = self.game.conditions.start_time
|
||||
|
||||
def run(self) -> None:
|
||||
self.reset()
|
||||
self.set_initial_flight_states()
|
||||
if self.game.settings.fast_forward_to_first_contact:
|
||||
self.simulate_until_first_contact()
|
||||
logging.info(f"Mission simulation completed at {self.time}")
|
||||
|
||||
def simulate_until_first_contact(self) -> None:
|
||||
while True:
|
||||
self.time += TICK
|
||||
if self.tick():
|
||||
return
|
||||
|
||||
def tick(self) -> bool:
|
||||
interrupt_sim = False
|
||||
for flight in self.iter_flights():
|
||||
if flight.on_game_tick(self.time, TICK):
|
||||
interrupt_sim = True
|
||||
|
||||
# TODO: Check for SAM or A2A contact.
|
||||
# Generate an engagement poly for all active air-to-air aircraft per-coalition
|
||||
# and compare those against aircraft positions. If any aircraft intersects an
|
||||
# enemy air-threat region, generate the mission. Also check against enemy SAM
|
||||
# zones.
|
||||
return interrupt_sim
|
||||
|
||||
def set_initial_flight_states(self) -> None:
|
||||
now = self.game.conditions.start_time
|
||||
for flight in self.iter_flights():
|
||||
estimator = TotEstimator(flight.package)
|
||||
start_time = estimator.mission_start_time(flight)
|
||||
if start_time <= timedelta():
|
||||
self.set_active_flight_state(flight, now)
|
||||
else:
|
||||
flight.set_state(
|
||||
WaitingForStart(flight, self.game.settings, now + start_time)
|
||||
)
|
||||
|
||||
def set_active_flight_state(self, flight: Flight, now: datetime) -> None:
|
||||
if flight.start_type is StartType.COLD:
|
||||
flight.set_state(StartUp(flight, self.game.settings, now))
|
||||
elif flight.start_type is StartType.WARM:
|
||||
flight.set_state(Taxi(flight, self.game.settings, now))
|
||||
elif flight.start_type is StartType.RUNWAY:
|
||||
flight.set_state(Takeoff(flight, self.game.settings, now))
|
||||
elif flight.start_type is StartType.IN_FLIGHT:
|
||||
flight.set_state(InFlight(flight, self.game.settings))
|
||||
else:
|
||||
raise ValueError(f"Unknown start type {flight.start_type} for {flight}")
|
||||
|
||||
def reset(self) -> None:
|
||||
self.time = self.game.conditions.start_time
|
||||
for flight in self.iter_flights():
|
||||
flight.set_state(Uninitialized(flight, self.game.settings))
|
||||
|
||||
def iter_flights(self) -> Iterator[Flight]:
|
||||
packages = itertools.chain(
|
||||
self.game.blue.ato.packages, self.game.red.ato.packages
|
||||
)
|
||||
for package in packages:
|
||||
yield from package.flights
|
||||
@ -6,6 +6,7 @@ from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.debriefing import Debriefing
|
||||
from game.missiongenerator import MissionGenerator
|
||||
from game.sim.aircraftsimulation import AircraftSimulation
|
||||
from game.sim.missionresultsprocessor import MissionResultsProcessor
|
||||
from game.unitmap import UnitMap
|
||||
|
||||
@ -17,9 +18,15 @@ class MissionSimulation:
|
||||
def __init__(self, game: Game) -> None:
|
||||
self.game = game
|
||||
self.unit_map: Optional[UnitMap] = None
|
||||
self.time = game.conditions.start_time
|
||||
|
||||
def run(self) -> None:
|
||||
sim = AircraftSimulation(self.game)
|
||||
sim.run()
|
||||
self.time = sim.time
|
||||
|
||||
def generate_miz(self, output: Path) -> None:
|
||||
self.unit_map = MissionGenerator(self.game).generate_miz(output)
|
||||
self.unit_map = MissionGenerator(self.game, self.time).generate_miz(output)
|
||||
|
||||
def debrief_current_state(
|
||||
self, state_path: Path, force_end: bool = False
|
||||
|
||||
@ -43,11 +43,11 @@ from .closestairfields import ObjectiveDistanceCache
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.ato.flightwaypoint import FlightWaypoint
|
||||
from game.ato.flight import Flight
|
||||
from .traveltime import GroundSpeed, TravelTime
|
||||
from .waypointbuilder import StrikeTarget, WaypointBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.package import Package
|
||||
from game.coalition import Coalition
|
||||
from game.threatzones import ThreatZones
|
||||
|
||||
@ -6,19 +6,17 @@ from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.utils import (
|
||||
Distance,
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL,
|
||||
Speed,
|
||||
kph,
|
||||
mach,
|
||||
meters,
|
||||
)
|
||||
from game.ato.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.package import Package
|
||||
|
||||
|
||||
|
||||
@ -18,10 +18,6 @@ from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import VehicleGroup, ShipGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
from game.transfers import MultiGroupTransport
|
||||
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
@ -31,7 +27,11 @@ from game.theater import (
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.ato.flightwaypoint import FlightWaypoint
|
||||
from game.ato.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.coalition import Coalition
|
||||
from game.transfers import MultiGroupTransport
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@ -278,6 +278,7 @@ class QTopPanel(QFrame):
|
||||
return
|
||||
|
||||
sim = MissionSimulation(self.game)
|
||||
sim.run()
|
||||
sim.generate_miz(persistency.mission_path_for("liberation_nextturn.miz"))
|
||||
|
||||
waiting = QWaitingForMissionResultWindow(self.game, sim, self)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user