This PR

- Introduces replanning of missions after continuing a turn.
- A new doctrine field setting the AEWC mission duration that was
previously hard coded.
This commit is contained in:
zhexu14 2025-10-17 22:23:55 +11:00 committed by GitHub
parent d09a15a7f3
commit c4a195646f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 111 additions and 14 deletions

View File

@ -12,6 +12,7 @@ from .flightmembers import FlightMembers
from .flightroster import FlightRoster from .flightroster import FlightRoster
from .flightstate import FlightState, Navigating, Uninitialized from .flightstate import FlightState, Navigating, Uninitialized
from .flightstate.killed import Killed from .flightstate.killed import Killed
from .flighttype import FlightType
from ..sidc import ( from ..sidc import (
Entity, Entity,
SidcDescribable, SidcDescribable,
@ -31,7 +32,6 @@ if TYPE_CHECKING:
from game.data.weapons import WeaponType from game.data.weapons import WeaponType
from .flightmember import FlightMember from .flightmember import FlightMember
from .flightplans.flightplan import FlightPlan from .flightplans.flightplan import FlightPlan
from .flighttype import FlightType
from .flightwaypoint import FlightWaypoint from .flightwaypoint import FlightWaypoint
from .package import Package from .package import Package
from .starttype import StartType from .starttype import StartType
@ -58,7 +58,8 @@ class Flight(SidcDescribable):
self.coalition = squadron.coalition self.coalition = squadron.coalition
self.squadron = squadron self.squadron = squadron
self.flight_type = flight_type self.flight_type = flight_type
self.squadron.claim_inventory(count) if flight_type != FlightType.IDLE:
self.squadron.claim_inventory(count)
if roster is None: if roster is None:
self.roster = FlightMembers(self, initial_size=count) self.roster = FlightMembers(self, initial_size=count)
else: else:

View File

@ -12,7 +12,7 @@ from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_m
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]): class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
@property @property
def patrol_duration(self) -> timedelta: def patrol_duration(self) -> timedelta:
return timedelta(hours=4) return self.flight.coalition.doctrine.aewc.duration
@property @property
def patrol_speed(self) -> Speed: def patrol_speed(self) -> Speed:

View File

@ -64,6 +64,7 @@ class FlightPlanBuilderTypes:
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(), FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
FlightType.FERRY: FerryFlightPlan.builder_type(), FlightType.FERRY: FerryFlightPlan.builder_type(),
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(), FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
FlightType.IDLE: BarCapFlightPlan.builder_type(),
} }
try: try:
return builder_dict[flight.flight_type] return builder_dict[flight.flight_type]

View File

@ -57,6 +57,7 @@ class FlightType(Enum):
REFUELING = "Refueling" REFUELING = "Refueling"
FERRY = "Ferry" FERRY = "Ferry"
AIR_ASSAULT = "Air Assault" AIR_ASSAULT = "Air Assault"
IDLE = "Idle"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value

View File

@ -47,6 +47,17 @@ class MissionScheduler:
margin=5 * 60, margin=5 * 60,
) )
for package in self.coalition.ato.packages: for package in self.coalition.ato.packages:
if package.time_over_target > datetime.min:
if package.primary_task in dca_types:
if (
package.mission_departure_time is not None
and package.mission_departure_time
> previous_cap_end_time[package.target]
):
previous_cap_end_time[package.target] = (
package.mission_departure_time
)
continue # If package already has TOT, leave it.
tot = TotEstimator(package).earliest_tot(now) tot = TotEstimator(package).earliest_tot(now)
if package.primary_task in dca_types: if package.primary_task in dca_types:
previous_end_time = previous_cap_end_time[package.target] previous_end_time = previous_cap_end_time[package.target]

View File

@ -24,6 +24,7 @@ from game.theater.theatergroundobject import (
VehicleGroupGroundObject, VehicleGroupGroundObject,
) )
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from game.ato.flighttype import FlightType
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -77,7 +78,8 @@ class TheaterState(WorldState["TheaterState"]):
self.threatening_air_defenses.remove(target) self.threatening_air_defenses.remove(target)
if target in self.detecting_air_defenses: if target in self.detecting_air_defenses:
self.detecting_air_defenses.remove(target) self.detecting_air_defenses.remove(target)
self.enemy_air_defenses.remove(target) if target in self.enemy_air_defenses:
self.enemy_air_defenses.remove(target)
self._rebuild_threat_zones() self._rebuild_threat_zones()
def eliminate_ship(self, target: NavalGroundObject) -> None: def eliminate_ship(self, target: NavalGroundObject) -> None:
@ -85,7 +87,8 @@ class TheaterState(WorldState["TheaterState"]):
self.threatening_air_defenses.remove(target) self.threatening_air_defenses.remove(target)
if target in self.detecting_air_defenses: if target in self.detecting_air_defenses:
self.detecting_air_defenses.remove(target) self.detecting_air_defenses.remove(target)
self.enemy_ships.remove(target) if target in self.enemy_ships:
self.enemy_ships.remove(target)
self._rebuild_threat_zones() self._rebuild_threat_zones()
def has_battle_position(self, target: VehicleGroupGroundObject) -> bool: def has_battle_position(self, target: VehicleGroupGroundObject) -> bool:
@ -155,21 +158,16 @@ class TheaterState(WorldState["TheaterState"]):
tracer, tracer,
) )
# Plan enough rounds of CAP that the target has coverage over the expected
# mission duration.
mission_duration = game.settings.desired_player_mission_duration.total_seconds()
barcap_duration = coalition.doctrine.cap.duration.total_seconds()
barcap_rounds = math.ceil(mission_duration / barcap_duration)
refueling_targets: list[MissionTarget] = [] refueling_targets: list[MissionTarget] = []
theater_refuling_point = finder.preferred_theater_refueling_control_point() theater_refuling_point = finder.preferred_theater_refueling_control_point()
if theater_refuling_point is not None: if theater_refuling_point is not None:
refueling_targets.append(theater_refuling_point) refueling_targets.append(theater_refuling_point)
return TheaterState( theater_state = TheaterState(
context=context, context=context,
barcaps_needed={ barcaps_needed={
cp: barcap_rounds for cp in finder.vulnerable_control_points() cp: cls._barcap_rounds(game, player, now, cp)
for cp in finder.vulnerable_control_points()
}, },
active_front_lines=list(finder.front_lines()), active_front_lines=list(finder.front_lines()),
front_line_stances={f: None for f in finder.front_lines()}, front_line_stances={f: None for f in finder.front_lines()},
@ -191,3 +189,62 @@ class TheaterState(WorldState["TheaterState"]):
enemy_barcaps=list(game.theater.control_points_for(not player)), enemy_barcaps=list(game.theater.control_points_for(not player)),
threat_zones=game.threat_zone_for(not player), threat_zones=game.threat_zone_for(not player),
) )
# Look through packages already planned in the ATO and eliminate from the
# list of targets.
for package in coalition.ato.packages:
if isinstance(package.target, NavalGroundObject):
theater_state.eliminate_ship(package.target)
if package.primary_task == FlightType.BAI and isinstance(
package.target, VehicleGroupGroundObject
):
theater_state.eliminate_battle_position(package.target)
if isinstance(package.target, IadsGroundObject):
theater_state.eliminate_air_defense(package.target)
if (
package.primary_task == FlightType.STRIKE
and isinstance(package.target, TheaterGroundObject)
and package.target in theater_state.strike_targets
):
theater_state.strike_targets.remove(package.target)
if package.primary_task == FlightType.AEWC:
# If a planned AEWC mission covers the target beyond the planned mission duration, it can safely be removed
if (
package.time_over_target + coalition.doctrine.aewc.duration
> now + game.settings.desired_player_mission_duration
) and package.target in theater_state.aewc_targets:
theater_state.aewc_targets.remove(package.target)
if (
package.primary_task
in (
FlightType.OCA_AIRCRAFT,
FlightType.OCA_RUNWAY,
)
and isinstance(package.target, ControlPoint)
and package.target in theater_state.oca_targets
):
theater_state.oca_targets.remove(package.target)
return theater_state
@classmethod
def _barcap_rounds(
cls, game: Game, player: bool, now: datetime, control_point: ControlPoint
) -> int:
"""Calculate number of additional rounds of CAP required to cover mission duration."""
coalition = game.coalition_for(player)
# Look through ATO for any existing planned CAP missions and calculate last planned CAP end
planned_cap_coverage_end_time = now
for package in coalition.ato.packages:
if package.target == control_point:
cap_end_time = (
package.time_over_target + coalition.doctrine.cap.duration
)
if cap_end_time > planned_cap_coverage_end_time:
planned_cap_coverage_end_time = cap_end_time
# When mission is expected to finish
mission_end_time = now + game.settings.desired_player_mission_duration
return math.ceil(
(mission_end_time - planned_cap_coverage_end_time).total_seconds()
/ coalition.doctrine.cap.duration.total_seconds()
)

View File

@ -52,6 +52,16 @@ class Helicopter:
) )
@dataclass
class Aewc:
#: The duration that AEWC flights will remain on-station
duration: timedelta
@staticmethod
def from_dict(data: dict[str, Any]) -> Aewc:
return Aewc(duration=timedelta(minutes=data["duration_minutes"]))
@dataclass @dataclass
class Cas: class Cas:
#: The duration that CAP flights will remain on-station. #: The duration that CAP flights will remain on-station.
@ -169,6 +179,9 @@ class Doctrine:
#: Helicopter specific doctrines. #: Helicopter specific doctrines.
helicopter: Helicopter helicopter: Helicopter
#: Doctrine for AEWC missions.
aewc: Aewc
#: Doctrine for CAS missions. #: Doctrine for CAS missions.
cas: Cas cas: Cas
@ -238,6 +251,7 @@ class Doctrine:
data["ground_unit_procurement_ratios"] data["ground_unit_procurement_ratios"]
), ),
helicopter=Helicopter.from_dict(data["helicopter"]), helicopter=Helicopter.from_dict(data["helicopter"]),
aewc=Aewc.from_dict(data["aewc"]),
cas=Cas.from_dict(data["cas"]), cas=Cas.from_dict(data["cas"]),
cap=Cap.from_dict(data["cap"]), cap=Cap.from_dict(data["cap"]),
sweep=Sweep.from_dict(data["sweep"]), sweep=Sweep.from_dict(data["sweep"]),

View File

@ -148,7 +148,7 @@ class AircraftGenerator:
faction.country, faction.country,
squadron, squadron,
1, 1,
FlightType.BARCAP, FlightType.IDLE,
StartType.COLD, StartType.COLD,
divert=None, divert=None,
) )

View File

@ -102,6 +102,12 @@ class MissionSimulation:
# Always skip combat as we are processing results from DCS. Any combat has already # Always skip combat as we are processing results from DCS. Any combat has already
# been resolved in-game # been resolved in-game
self.tick(events, CombatResolutionMethod.SKIP, force_continue=True) self.tick(events, CombatResolutionMethod.SKIP, force_continue=True)
self.game.blue.plan_missions(self.game.simulation_time)
self.game.red.plan_missions(self.game.simulation_time)
self.game.game_stats.update(self.game)
# Generate begin_new_turn event which triggers a refresh of the React map screen to
# show newly planned missions.
events.begin_new_turn()
def finish(self) -> None: def finish(self) -> None:
self.unit_map = None self.unit_map = None

View File

@ -6,6 +6,8 @@ max_ingress_distance_nm: 30
min_ingress_distance_nm: 10 min_ingress_distance_nm: 10
rendezvous_altitude_ft_msl: 22000 rendezvous_altitude_ft_msl: 22000
combat_altitude_ft_msl: 18000 combat_altitude_ft_msl: 18000
aewc:
duration_minutes: 240
cap: cap:
duration_minutes: 30 duration_minutes: 30
min_track_length_nm: 12 min_track_length_nm: 12

View File

@ -6,6 +6,8 @@ max_ingress_distance_nm: 45
min_ingress_distance_nm: 10 min_ingress_distance_nm: 10
rendezvous_altitude_ft_msl: 25000 rendezvous_altitude_ft_msl: 25000
combat_altitude_ft_msl: 20000 combat_altitude_ft_msl: 20000
aewc:
duration_minutes: 240
cap: cap:
duration_minutes: 30 duration_minutes: 30
min_track_length_nm: 15 min_track_length_nm: 15

View File

@ -6,6 +6,8 @@ max_ingress_distance_nm: 7
min_ingress_distance_nm: 5 min_ingress_distance_nm: 5
rendezvous_altitude_ft_msl: 10000 rendezvous_altitude_ft_msl: 10000
combat_altitude_ft_msl: 8000 combat_altitude_ft_msl: 8000
aewc:
duration_minutes: 240
cap: cap:
duration_minutes: 30 duration_minutes: 30
min_track_length_nm: 8 min_track_length_nm: 8