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

View File

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

View File

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

View File

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

View File

@ -47,6 +47,17 @@ class MissionScheduler:
margin=5 * 60,
)
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)
if package.primary_task in dca_types:
previous_end_time = previous_cap_end_time[package.target]

View File

@ -24,6 +24,7 @@ from game.theater.theatergroundobject import (
VehicleGroupGroundObject,
)
from game.threatzones import ThreatZones
from game.ato.flighttype import FlightType
if TYPE_CHECKING:
from game import Game
@ -77,7 +78,8 @@ class TheaterState(WorldState["TheaterState"]):
self.threatening_air_defenses.remove(target)
if target in self.detecting_air_defenses:
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()
def eliminate_ship(self, target: NavalGroundObject) -> None:
@ -85,7 +87,8 @@ class TheaterState(WorldState["TheaterState"]):
self.threatening_air_defenses.remove(target)
if target in self.detecting_air_defenses:
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()
def has_battle_position(self, target: VehicleGroupGroundObject) -> bool:
@ -155,21 +158,16 @@ class TheaterState(WorldState["TheaterState"]):
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] = []
theater_refuling_point = finder.preferred_theater_refueling_control_point()
if theater_refuling_point is not None:
refueling_targets.append(theater_refuling_point)
return TheaterState(
theater_state = TheaterState(
context=context,
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()),
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)),
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
class Cas:
#: The duration that CAP flights will remain on-station.
@ -169,6 +179,9 @@ class Doctrine:
#: Helicopter specific doctrines.
helicopter: Helicopter
#: Doctrine for AEWC missions.
aewc: Aewc
#: Doctrine for CAS missions.
cas: Cas
@ -238,6 +251,7 @@ class Doctrine:
data["ground_unit_procurement_ratios"]
),
helicopter=Helicopter.from_dict(data["helicopter"]),
aewc=Aewc.from_dict(data["aewc"]),
cas=Cas.from_dict(data["cas"]),
cap=Cap.from_dict(data["cap"]),
sweep=Sweep.from_dict(data["sweep"]),

View File

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

View File

@ -102,6 +102,12 @@ class MissionSimulation:
# Always skip combat as we are processing results from DCS. Any combat has already
# been resolved in-game
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:
self.unit_map = None

View File

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

View File

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

View File

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