mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9520b76246 | ||
|
|
67183c31da | ||
|
|
54759f1f94 | ||
|
|
0fcf308079 | ||
|
|
1ad27e27ea | ||
|
|
eae5e1af30 | ||
|
|
b268d09432 | ||
|
|
220b83a22d | ||
|
|
83530af35d | ||
|
|
00475d767b | ||
|
|
1a8faff717 | ||
|
|
68e0d43a1d | ||
|
|
0510af8b6b | ||
|
|
c54ac4e21a | ||
|
|
bbc41f4c1a | ||
|
|
8bc8eb0e28 | ||
|
|
86f647cc6d | ||
|
|
2f16916ac4 | ||
|
|
06044dd4c3 | ||
|
|
c46cce01b6 | ||
|
|
d78e570d9d | ||
|
|
f4a1908088 | ||
|
|
23341e69fa | ||
|
|
11fc28ad3a | ||
|
|
9b96b7be26 | ||
|
|
f7ee923cbf | ||
|
|
3d788e3ae8 | ||
|
|
624f2a00fd | ||
|
|
6c3cbbf0de | ||
|
|
96d0b2b7d9 | ||
|
|
855d269420 | ||
|
|
c0748e2a3e | ||
|
|
9f10ecc884 | ||
|
|
31c80dfd02 | ||
|
|
362ce66f80 | ||
|
|
96ee73dd50 | ||
|
|
f82fc3a930 | ||
|
|
cf575fba2a | ||
|
|
96f940bc59 | ||
|
|
98c71c255b | ||
|
|
0e5567ceee | ||
|
|
2832ece316 | ||
|
|
5cfd6c86f3 | ||
|
|
27ef269435 | ||
|
|
8b46ef5e35 | ||
|
|
e243d074ff | ||
|
|
cc39e6579b | ||
|
|
d7eaf1330f | ||
|
|
c44ea0025d | ||
|
|
1216c90891 | ||
|
|
ea80ddc321 | ||
|
|
121ca20edb | ||
|
|
e4586add72 | ||
|
|
32b2e40b37 | ||
|
|
e8190748eb | ||
|
|
d2aa615133 | ||
|
|
756602e682 | ||
|
|
204950e487 | ||
|
|
6290607eb6 | ||
|
|
afad73ac27 | ||
|
|
52fbc75e1c | ||
|
|
5c5af40bd5 | ||
|
|
d1f66ed040 | ||
|
|
6467193c88 | ||
|
|
b2670ba30b | ||
|
|
dcf4da57b3 | ||
|
|
6f825db086 | ||
|
|
afa9122885 | ||
|
|
df8e09a1ad | ||
|
|
5d038e8247 | ||
|
|
ce70952ee0 | ||
|
|
1d16aba678 | ||
|
|
f3156ab51a | ||
|
|
ed9fa37341 | ||
|
|
12aa1ee287 | ||
|
|
7e755f1eb1 | ||
|
|
3da1689767 | ||
|
|
0671e45dde | ||
|
|
c15b4ac83f | ||
|
|
f1eb514295 | ||
|
|
df8f1c4291 | ||
|
|
c32525b542 | ||
|
|
db14b691cc | ||
|
|
052a04ca05 | ||
|
|
ad100a1a17 | ||
|
|
4412a0fa91 | ||
|
|
23a7add2f1 | ||
|
|
040362b333 | ||
|
|
4155afc69a | ||
|
|
cf84c03d8f | ||
|
|
336384cd74 | ||
|
|
da7d3a3f1a | ||
|
|
1231cb145d | ||
|
|
47488a0bdd | ||
|
|
015859e91e | ||
|
|
9c0d114085 | ||
|
|
f907356f93 | ||
|
|
fcb610423b | ||
|
|
3086f64e98 | ||
|
|
5592a834c9 | ||
|
|
1e48affc1c | ||
|
|
f27b6e28e0 | ||
|
|
d0ff271a79 | ||
|
|
9a00a60cd4 | ||
|
|
5254ef3442 | ||
|
|
fe8efbe905 | ||
|
|
48040d7306 | ||
|
|
f3ed25b5b2 | ||
|
|
2d15db1932 | ||
|
|
1fa1f830d4 | ||
|
|
0c0291fc5e | ||
|
|
24c6b62ad4 | ||
|
|
8c9343e0db | ||
|
|
a33da01af6 | ||
|
|
ce8ccb0227 | ||
|
|
68a9dbd141 | ||
|
|
4c3f0a82d9 | ||
|
|
dc85287cd8 | ||
|
|
65f4a2cf86 | ||
|
|
02de6d325a | ||
|
|
7b16967641 | ||
|
|
0e7ea0050f | ||
|
|
ec0e64eaf4 | ||
|
|
b12169f180 | ||
|
|
3a5d0e831c | ||
|
|
903a6c4d5f | ||
|
|
e36f07a3ff | ||
|
|
2d65a038ee | ||
|
|
09617adc56 | ||
|
|
01832b6594 | ||
|
|
30ad71f93b | ||
|
|
4f8ce77e50 | ||
|
|
464ece60da | ||
|
|
cafaea897d | ||
|
|
58820dd6c6 | ||
|
|
8e845e6a27 | ||
|
|
e1b55816e3 | ||
|
|
e95661c6a2 | ||
|
|
c96e0d8f28 | ||
|
|
dfb65facb5 | ||
|
|
8c6ca4de5f | ||
|
|
1c3dee2098 | ||
|
|
170e432008 |
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -31,7 +31,7 @@ body:
|
||||
If the bug was found in a test/development build, select "Test build" or "Development build"
|
||||
and provide a link to the applicable build in the field below.
|
||||
options:
|
||||
- v1.3.1
|
||||
- v1.4.1
|
||||
- Test build
|
||||
- type: input
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
@@ -39,7 +39,7 @@ body:
|
||||
If the bug was found in a test/development build, select "Test build" or "Development build"
|
||||
and provide a link to the applicable build in the field below.
|
||||
options:
|
||||
- 1.3.1
|
||||
- 1.4.1
|
||||
- Test build
|
||||
- type: input
|
||||
attributes:
|
||||
|
||||
@@ -68,7 +68,7 @@ Our plans for future releases can be found on our
|
||||
[Projects page](https://github.com/dcs-retribution/dcs-retribution/projects).
|
||||
Each planned release has a Project, and the page for that project has columns for to do,
|
||||
in progress, and done. Items in the Done column are in the
|
||||
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Betas)
|
||||
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds)
|
||||
for that release. Items in the To do column are planned to be added to that release.
|
||||
|
||||
## Resources
|
||||
|
||||
38
changelog.md
38
changelog.md
@@ -1,3 +1,41 @@
|
||||
# Retribution v1.5.0
|
||||
|
||||
## Features/Improvements
|
||||
* **[Campaigns]** Ability to define invisible FOBs
|
||||
* **[Campaigns]** Ability to define influence zones for Control Points
|
||||
* **[Plugins]** Improvements to AI support for EW Script 2.0
|
||||
* **[Plugins]** Add moose as a base plugin
|
||||
* **[Config]** New preference setting to trigger the first-start window on every start (could help in scenarios multiple Retribution instances need to run concurrently)
|
||||
* **[Modding]** Update Grinelli Designs F-22A Mod to 2.0.0 (May 2025)
|
||||
* **[Campaign Design]** Added support for Germany Cold War terrain by Ugra Media
|
||||
* **[Options]** New option to control EPLRS-task injection in mission generator
|
||||
* **[Modding]** Add Tornado F3 ADV (v1.0, requires FC3 fix)
|
||||
* **[Modding]** Add Military Aircraft Mod 1.7.2 (See VSN Discord)
|
||||
* **[Payload Editor]** Add Sniper POD
|
||||
* **[Modding]** Update SU-30 mod to v2.8.04 Beta + CWS 3.72
|
||||
* **[Plugins]** EW Script - DEAD added to applicable flight types.
|
||||
* **[Plugins]** EW Script - Offensive Jamming restricted to aircraft with ALQ99/249 pods, or "has_built_in_jamming: true" in aircarft yaml (AI and Player)
|
||||
* **[Modding]** Update to CJS Super Hornet Mod v2.4.4
|
||||
* **[Engine]** Support for DCS 2.9.20.15010, including MiG-29A Fulcrum support.
|
||||
* **[Campaigns]** Ability to define neutral FOBs and Airfields
|
||||
* **[Modding]** Add Airboss Moose Module
|
||||
* **[AirWing]** Use aircraft display names for easier differentiation between modules
|
||||
|
||||
## Fixes
|
||||
* **[Flight Plans]** Fixed bugs wrt planning escort flights
|
||||
* **[Flight Plans]** Added AntiShipStrike as a fallback task for OCA/Aircraft to fix a bug where the S-3B could not do OCA/Aircraft
|
||||
* **[Squadrons]** Fixed a bug where loading an air wing config would not properly load all squadrons
|
||||
* **[Flight Plans]** Fixed a bug where SEAD flights would fire one ARM and RTB
|
||||
* **[Plugins]** EW Script - Fix radar detection routine.
|
||||
* **[Campaign]** Fixed a bug where sinking a destroyer in a carrier group would cause squadrons to be removed from the carrier
|
||||
* **[Engine]** Fixed a bug with transfers to Helipads
|
||||
* **[Engine]** Fixed a bug with parking allocation
|
||||
* **[Flight Plans]** Fixed a bug where divert airfield was broken for opfor
|
||||
* **[Engine]** Fixed a bug with state.json loading wrt transfers
|
||||
* **[Engine]** Fixed a bug wrt pretense generation and moose script conflicts
|
||||
* **[Engine]** Fixed a bug where frontline debriefing was not properly calculated
|
||||
* **[Mission Generation]** Fixed an issue where blue and red units spawned next to each other on frontlines
|
||||
|
||||
# Retribution v1.4.1 (hotfix)
|
||||
|
||||
## Fixes
|
||||
|
||||
22917
client/package-lock.json
generated
22917
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -66,7 +66,7 @@
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/redux-logger": "^3.0.13",
|
||||
"@types/websocket": "^1.0.10",
|
||||
"electron": "^22.3.27",
|
||||
"electron": "^28.3.2",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"generate-license-file": "^2.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
|
||||
@@ -60,7 +60,7 @@ const WaypointMarker = (props: WaypointMarkerProps) => {
|
||||
useEffect(() => {
|
||||
const waypoint = props.waypoint;
|
||||
marker.current?.setTooltipContent(
|
||||
`${props.number} ${waypoint.name}<br />` +
|
||||
`${props.number-1} ${waypoint.name}<br />` +
|
||||
`${waypoint.altitude_ft.toFixed()} ft ${waypoint.altitude_reference}<br />` +
|
||||
waypoint.timing
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
project = "DCS Retribution"
|
||||
copyright = "2025, DCS Retribution Team"
|
||||
author = "DCS Retribution Team"
|
||||
release = "1.4.1"
|
||||
release = "1.5.0"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
@@ -40,7 +40,7 @@ class ArmedForces:
|
||||
# all layouts which are usable by the given faction.
|
||||
for layout in LAYOUTS.layouts:
|
||||
if layout.generic and layout.usable_by_faction(faction):
|
||||
# Creates a faction compatible GorceGroup
|
||||
# Creates a faction compatible ForceGroup
|
||||
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
|
||||
|
||||
# Add all preset groups afterwards to prevent them being merged with generics
|
||||
|
||||
@@ -258,6 +258,9 @@ class ForceGroup:
|
||||
if unit_count == 0:
|
||||
# No units to be created so dont create a theater group for them
|
||||
return
|
||||
# If the control point is neutral, don't create any units at all
|
||||
if ground_object.control_point.captured.is_neutral:
|
||||
return
|
||||
# Generate Units
|
||||
fixed_pos = FIXED_POS_ARG in unit_group.name
|
||||
fixed_hdg = FIXED_HDG_ARG in unit_group.name
|
||||
|
||||
@@ -35,6 +35,7 @@ if TYPE_CHECKING:
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.sim.simulationresults import SimulationResults
|
||||
from game.squadrons import Squadron, Pilot
|
||||
from game.theater.player import Player
|
||||
from game.transfers import TransferOrder
|
||||
from game.data.weapons import WeaponType
|
||||
from .flightmember import FlightMember
|
||||
@@ -174,7 +175,7 @@ class Flight(
|
||||
self.roster = FlightMembers.from_roster(self, self.roster)
|
||||
|
||||
@property
|
||||
def blue(self) -> bool:
|
||||
def blue(self) -> Player:
|
||||
return self.squadron.player
|
||||
|
||||
@property
|
||||
|
||||
@@ -23,7 +23,13 @@ class EscortFlightPlan(FormationAttackFlightPlan):
|
||||
|
||||
@property
|
||||
def split_time(self) -> datetime:
|
||||
if self.package.primary_flight and self.package.primary_flight.flight_plan:
|
||||
# Avoid infinite recursion when this escort flight is itself the primary flight
|
||||
# This can happen when only escort flights remain in a package
|
||||
if (
|
||||
self.package.primary_flight
|
||||
and self.package.primary_flight != self.flight
|
||||
and self.package.primary_flight.flight_plan
|
||||
):
|
||||
return self.package.primary_flight.flight_plan.mission_departure_time
|
||||
else:
|
||||
return super().split_time
|
||||
|
||||
@@ -11,7 +11,7 @@ from ..packagewaypoints import PackageWaypoints
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import ConflictTheater
|
||||
from game.theater import ConflictTheater, Player
|
||||
from game.threatzones import ThreatZones
|
||||
from ..flight import Flight
|
||||
from ..package import Package
|
||||
@@ -38,7 +38,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
||||
self._generate_package_waypoints_if_needed(dump_debug_info)
|
||||
self._flight_plan = self.build(dump_debug_info)
|
||||
except NavMeshError as ex:
|
||||
color = "blue" if self.flight.squadron.player else "red"
|
||||
color = "blue" if self.flight.squadron.player.is_blue else "red"
|
||||
raise PlanningError(
|
||||
f"Could not plan {color} {self.flight.flight_type.value} from "
|
||||
f"{self.flight.departure} to {self.package.target}"
|
||||
@@ -71,7 +71,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
||||
return self.flight.coalition
|
||||
|
||||
@property
|
||||
def is_player(self) -> bool:
|
||||
def is_player(self) -> Player:
|
||||
return self.coalition.player
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
@@ -26,6 +25,7 @@ class SquadronConfig:
|
||||
name: Optional[str]
|
||||
nickname: Optional[str]
|
||||
female_pilot_percentage: Optional[int]
|
||||
aircraft_type: Optional[str]
|
||||
|
||||
@property
|
||||
def auto_assignable(self) -> set[FlightType]:
|
||||
@@ -51,6 +51,7 @@ class SquadronConfig:
|
||||
data.get("name", None),
|
||||
data.get("nickname", None),
|
||||
data.get("female_pilot_percentage", None),
|
||||
data.get("aircraft_type", None),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -83,15 +84,14 @@ class CampaignAirWingConfig:
|
||||
try:
|
||||
base = theater.control_point_named(base_id)
|
||||
except:
|
||||
if base_id == "Red CV":
|
||||
base = next((c for c in carriers if not c.captured), None)
|
||||
elif base_id == "Blue CV":
|
||||
base = next((c for c in carriers if c.captured), None)
|
||||
elif base_id == "Red LHA":
|
||||
base = next((l for l in lhas if not l.captured), None)
|
||||
elif base_id == "Blue LHA":
|
||||
base = next((l for l in lhas if l.captured), None)
|
||||
|
||||
logging.warning(
|
||||
f"Control point {base_id} not found, trying to match by full name"
|
||||
)
|
||||
if not base:
|
||||
try:
|
||||
base = theater.control_point_by_full_name(base_id)
|
||||
except KeyError:
|
||||
logging.error(f"Control point {base_id} not found, skipping")
|
||||
for squadron_data in squadron_configs:
|
||||
if base is None:
|
||||
logging.warning(
|
||||
|
||||
@@ -60,7 +60,7 @@ class DefaultSquadronAssigner:
|
||||
) -> Optional[SquadronDef]:
|
||||
for preferred_aircraft in config.aircraft:
|
||||
squadron_def = self.find_preferred_squadron(
|
||||
preferred_aircraft, config.primary, control_point
|
||||
preferred_aircraft, config.aircraft_type, config.primary, control_point
|
||||
)
|
||||
if squadron_def is not None:
|
||||
return squadron_def
|
||||
@@ -78,7 +78,11 @@ class DefaultSquadronAssigner:
|
||||
)
|
||||
|
||||
def find_preferred_squadron(
|
||||
self, preferred_aircraft: str, task: FlightType, control_point: ControlPoint
|
||||
self,
|
||||
preferred_aircraft: str,
|
||||
aircraft_type: Optional[str],
|
||||
task: FlightType,
|
||||
control_point: ControlPoint,
|
||||
) -> Optional[SquadronDef]:
|
||||
# Attempt to find a squadron with the name in the request.
|
||||
squadron_def = self.find_squadron_by_name(
|
||||
@@ -90,7 +94,7 @@ class DefaultSquadronAssigner:
|
||||
# If the name didn't match a squadron available to this coalition, try to find
|
||||
# an aircraft with the matching name that meets the requirements.
|
||||
try:
|
||||
aircraft = AircraftType.named(preferred_aircraft)
|
||||
aircraft = AircraftType.named(aircraft_type or preferred_aircraft)
|
||||
except KeyError:
|
||||
logging.warning(
|
||||
"%s is neither a compatible squadron or a known aircraft type, "
|
||||
|
||||
@@ -16,6 +16,7 @@ from dcs.terrain import Airport
|
||||
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||
|
||||
from game.controlpoint_influenceradius import ControlPointInfluenceRadius, point_in_zone
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.positioned import Positioned
|
||||
from game.profiling import logged_duration
|
||||
@@ -24,6 +25,8 @@ from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
Carrier,
|
||||
ControlPoint,
|
||||
ControlPointType,
|
||||
Player,
|
||||
Fob,
|
||||
Lha,
|
||||
OffMapSpawn,
|
||||
@@ -53,6 +56,8 @@ class MizCampaignLoader:
|
||||
|
||||
FOB_UNIT_TYPE = Unarmed.SKP_11.id
|
||||
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD", "FARP"]
|
||||
INVISIBLE_FOB_UNIT_TYPE = Unarmed.M_818.id
|
||||
NEUTRAL_FOB_UNIT_TYPE = Unarmed.KrAZ6322.id
|
||||
|
||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
||||
@@ -120,9 +125,13 @@ class MizCampaignLoader:
|
||||
def control_point_from_airport(
|
||||
self, airport: Airport, ctld_zones: List[Tuple[Point, float]]
|
||||
) -> ControlPoint:
|
||||
cp = Airfield(
|
||||
airport, self.theater, starts_blue=airport.is_blue(), ctld_zones=ctld_zones
|
||||
)
|
||||
if airport.dynamic_spawn:
|
||||
starting_coalition = Player.NEUTRAL
|
||||
elif airport.is_blue():
|
||||
starting_coalition = Player.BLUE
|
||||
else:
|
||||
starting_coalition = Player.RED
|
||||
cp = Airfield(airport, self.theater, starting_coalition, ctld_zones=ctld_zones)
|
||||
|
||||
# Use the unlimited aircraft option to determine if an airfield should
|
||||
# be owned by the player when the campaign is "inverted".
|
||||
@@ -130,42 +139,54 @@ class MizCampaignLoader:
|
||||
|
||||
return cp
|
||||
|
||||
def country(self, blue: bool) -> Country:
|
||||
country = self.mission.country(
|
||||
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
|
||||
)
|
||||
def country(self, blue: Player) -> Country:
|
||||
if blue.is_blue:
|
||||
country = self.mission.country(self.BLUE_COUNTRY.name)
|
||||
else:
|
||||
country = self.mission.country(self.RED_COUNTRY.name)
|
||||
# Should be guaranteed because we initialized them.
|
||||
assert country
|
||||
return country
|
||||
|
||||
@property
|
||||
def blue(self) -> Country:
|
||||
return self.country(blue=True)
|
||||
return self.country(blue=Player.BLUE)
|
||||
|
||||
@property
|
||||
def red(self) -> Country:
|
||||
return self.country(blue=False)
|
||||
return self.country(blue=Player.RED)
|
||||
|
||||
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]:
|
||||
def off_map_spawns(self, blue: Player) -> Iterator[PlaneGroup]:
|
||||
for group in self.country(blue).plane_group:
|
||||
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
|
||||
def carriers(self, blue: Player) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.CV_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
|
||||
def lhas(self, blue: Player) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.LHA_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
|
||||
def fobs(self, blue: Player) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue).vehicle_group:
|
||||
if group.units[0].type == self.FOB_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def invisible_fobs(self, blue: Player) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue).vehicle_group:
|
||||
if group.units[0].type == self.INVISIBLE_FOB_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def neutral_fobs(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type == self.NEUTRAL_FOB_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ships(self) -> Iterator[ShipGroup]:
|
||||
for group in self.red.ship_group:
|
||||
@@ -272,16 +293,22 @@ class MizCampaignLoader:
|
||||
def scenery(self) -> List[SceneryGroup]:
|
||||
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
|
||||
|
||||
@cached_property
|
||||
def cp_influence_zones(self) -> List[ControlPointInfluenceRadius]:
|
||||
return ControlPointInfluenceRadius.from_trigger_zones(
|
||||
self.mission.triggers._zones
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def control_points(self) -> dict[UUID, ControlPoint]:
|
||||
control_points = {}
|
||||
for airport in self.mission.terrain.airport_list():
|
||||
if airport.is_blue() or airport.is_red():
|
||||
if airport.is_blue() or airport.is_red() or airport.is_neutral():
|
||||
ctld_zones = self.get_ctld_zones(airport.name)
|
||||
control_point = self.control_point_from_airport(airport, ctld_zones)
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
for blue in (False, True):
|
||||
for blue in (Player.RED, Player.BLUE):
|
||||
for group in self.off_map_spawns(blue):
|
||||
control_point = OffMapSpawn(
|
||||
str(group.name), group.position, self.theater, starts_blue=blue
|
||||
@@ -312,17 +339,47 @@ class MizCampaignLoader:
|
||||
control_point.captured_invert = fob.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
for fob in self.invisible_fobs(blue):
|
||||
ctld_zones = self.get_ctld_zones(fob.name)
|
||||
control_point = Fob(
|
||||
str(fob.name),
|
||||
fob.position,
|
||||
self.theater,
|
||||
starts_blue=blue,
|
||||
ctld_zones=ctld_zones,
|
||||
is_invisible=True,
|
||||
)
|
||||
control_point.captured_invert = fob.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
for fob in self.neutral_fobs:
|
||||
ctld_zones = self.get_ctld_zones(fob.name)
|
||||
control_point = Fob(
|
||||
str(fob.name),
|
||||
fob.position,
|
||||
self.theater,
|
||||
starts_blue=Player.NEUTRAL,
|
||||
ctld_zones=ctld_zones,
|
||||
)
|
||||
control_point.captured_invert = fob.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
if self.cp_influence_zones:
|
||||
for cp in control_points.values():
|
||||
for influence_radius in self.cp_influence_zones:
|
||||
if cp.full_name == influence_radius.cp_name:
|
||||
cp.influence_radius = influence_radius
|
||||
return control_points
|
||||
|
||||
@property
|
||||
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue=True).vehicle_group:
|
||||
for group in self.country(blue=Player.BLUE).vehicle_group:
|
||||
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def shipping_lane_groups(self) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue=True).ship_group:
|
||||
for group in self.country(blue=Player.BLUE).ship_group:
|
||||
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@@ -346,7 +403,7 @@ class MizCampaignLoader:
|
||||
|
||||
@property
|
||||
def cp_convoy_spawns(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue=True).vehicle_group:
|
||||
for group in self.country(blue=Player.BLUE).vehicle_group:
|
||||
if group.units[0].type == self.CP_CONVOY_SPAWN_TYPE:
|
||||
yield group
|
||||
|
||||
@@ -471,7 +528,59 @@ class MizCampaignLoader:
|
||||
def objective_info(
|
||||
self, near: Positioned, allow_naval: bool = False
|
||||
) -> Tuple[ControlPoint, Distance]:
|
||||
closest = self.theater.closest_control_point(near.position, allow_naval)
|
||||
zones_containing_point = [
|
||||
z
|
||||
for z in self.cp_influence_zones
|
||||
if point_in_zone(z.zone_def, near.position)
|
||||
]
|
||||
|
||||
# Ensure we only consider naval control points if allow_naval is True
|
||||
candidates = [
|
||||
self.theater.control_point_named(z.cp_name) for z in zones_containing_point
|
||||
]
|
||||
if not allow_naval:
|
||||
candidates = [
|
||||
cp
|
||||
for cp in candidates
|
||||
if cp.cptype
|
||||
not in [
|
||||
ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
ControlPointType.LHA_GROUP,
|
||||
]
|
||||
]
|
||||
|
||||
if candidates:
|
||||
closest = min(
|
||||
candidates, key=lambda cp: cp.position.distance_to_point(near.position)
|
||||
)
|
||||
distance = meters(closest.position.distance_to_point(near.position))
|
||||
return closest, distance
|
||||
|
||||
# If no zones contain the point, find the closest control point without an influence radius
|
||||
if not allow_naval:
|
||||
fallback_candidates = [
|
||||
cp
|
||||
for cp in self.theater.controlpoints
|
||||
if cp.cptype
|
||||
not in [
|
||||
ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
ControlPointType.LHA_GROUP,
|
||||
]
|
||||
]
|
||||
else:
|
||||
fallback_candidates = self.theater.controlpoints
|
||||
|
||||
fallback_candidates = [
|
||||
cp for cp in fallback_candidates if not cp.influence_radius
|
||||
]
|
||||
if not fallback_candidates:
|
||||
raise RuntimeError(
|
||||
f"All control points have an influence zone but no zones contain {near} at {near.position}"
|
||||
)
|
||||
closest = min(
|
||||
fallback_candidates,
|
||||
key=lambda cp: cp.position.distance_to_point(near.position),
|
||||
)
|
||||
distance = meters(closest.position.distance_to_point(near.position))
|
||||
return closest, distance
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from game.procurement import AircraftProcurementRequest, ProcurementAi
|
||||
from game.profiling import MultiEventTracer, logged_duration
|
||||
from game.squadrons import AirWing
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.theater.player import Player
|
||||
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||
from game.threatzones import ThreatZones
|
||||
from game.transfers import PendingTransfers
|
||||
@@ -32,7 +33,7 @@ if TYPE_CHECKING:
|
||||
|
||||
class Coalition:
|
||||
def __init__(
|
||||
self, game: Game, faction: Faction, budget: float, player: bool
|
||||
self, game: Game, faction: Faction, budget: float, player: Player
|
||||
) -> None:
|
||||
self.game = game
|
||||
self.player = player
|
||||
@@ -68,9 +69,11 @@ class Coalition:
|
||||
|
||||
@property
|
||||
def coalition_id(self) -> int:
|
||||
if self.player:
|
||||
if self.player.is_blue:
|
||||
return 2
|
||||
return 1
|
||||
elif self.player.is_red:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
@property
|
||||
def opponent(self) -> Coalition:
|
||||
@@ -95,12 +98,23 @@ class Coalition:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting any volatile types that can be deterministically
|
||||
# recomputed on load for the sake of save compatibility.
|
||||
del state["_threat_zone"]
|
||||
del state["_navmesh"]
|
||||
del state["faker"]
|
||||
# TODO: Figure out why this is needed after adding neutral point support
|
||||
if state["player"] != Player.NEUTRAL:
|
||||
del state["_threat_zone"]
|
||||
del state["_navmesh"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Migration: Convert old boolean player values to Player enum
|
||||
if "player" in state and isinstance(state["player"], bool):
|
||||
from game.theater.player import Player
|
||||
|
||||
if state["player"]:
|
||||
state["player"] = Player.BLUE
|
||||
else:
|
||||
state["player"] = Player.RED
|
||||
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.on_load()
|
||||
@@ -203,7 +217,7 @@ class Coalition:
|
||||
squadron.refund_orders()
|
||||
|
||||
def plan_missions(self, now: datetime) -> None:
|
||||
color = "Blue" if self.player else "Red"
|
||||
color = "Blue" if self.player.is_blue else "Red"
|
||||
with MultiEventTracer() as tracer:
|
||||
with tracer.trace(f"{color} mission planning"):
|
||||
with tracer.trace(f"{color} mission identification"):
|
||||
@@ -220,7 +234,7 @@ class Coalition:
|
||||
# to ground forces and 1400 to aircraft. After that the budget will be spent
|
||||
# proportionally based on how much is already invested.
|
||||
|
||||
if self.player:
|
||||
if self.player.is_blue:
|
||||
manage_runways = self.game.settings.automate_runway_repair
|
||||
manage_front_line = self.game.settings.automate_front_line_reinforcements
|
||||
manage_aircraft = self.game.settings.automate_aircraft_reinforcements
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import math
|
||||
import operator
|
||||
from collections.abc import Iterable, Iterator
|
||||
@@ -16,6 +17,7 @@ from game.theater import (
|
||||
OffMapSpawn,
|
||||
ParkingType,
|
||||
NavalControlPoint,
|
||||
Player,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
@@ -35,7 +37,7 @@ MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
|
||||
class ObjectiveFinder:
|
||||
"""Identifies potential objectives for the mission planner."""
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
def __init__(self, game: Game, is_player: Player) -> None:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
|
||||
@@ -154,7 +156,7 @@ class ObjectiveFinder:
|
||||
airfields_in_proximity = self.closest_airfields_to(cp)
|
||||
airbase_threat_range = self.game.settings.airbase_threat_range
|
||||
if (
|
||||
not self.is_player
|
||||
self.is_player.is_red
|
||||
and randint(1, 100)
|
||||
> self.game.settings.opfor_autoplanner_aggressiveness
|
||||
):
|
||||
@@ -216,7 +218,7 @@ class ObjectiveFinder:
|
||||
|
||||
def farthest_friendly_control_point(self) -> ControlPoint:
|
||||
"""Finds the friendly control point that is farthest from any threats."""
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
threat_zones = self.game.threat_zone_for(self.is_player.opponent)
|
||||
|
||||
farthest = None
|
||||
max_distance = meters(0)
|
||||
@@ -234,7 +236,7 @@ class ObjectiveFinder:
|
||||
|
||||
def closest_friendly_control_point(self) -> ControlPoint:
|
||||
"""Finds the friendly control point that is closest to any threats."""
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
threat_zones = self.game.threat_zone_for(self.is_player.opponent)
|
||||
|
||||
closest = None
|
||||
min_distance = meters(math.inf)
|
||||
@@ -258,14 +260,37 @@ class ObjectiveFinder:
|
||||
return (
|
||||
c
|
||||
for c in self.game.theater.controlpoints
|
||||
if not c.is_friendly(self.is_player)
|
||||
if not c.is_friendly(self.is_player) and c.captured != Player.NEUTRAL
|
||||
)
|
||||
|
||||
def prioritized_points(self) -> list[ControlPoint]:
|
||||
prioritized = []
|
||||
capturable_later = []
|
||||
isolated = []
|
||||
for cp in self.game.theater.control_points_for(not self.is_player):
|
||||
for cp in self.game.theater.control_points_for(self.is_player.opponent):
|
||||
if cp.is_isolated:
|
||||
isolated.append(cp)
|
||||
continue
|
||||
if cp.has_active_frontline:
|
||||
prioritized.append(cp)
|
||||
else:
|
||||
capturable_later.append(cp)
|
||||
prioritized.extend(self._targets_by_range(capturable_later))
|
||||
prioritized.extend(self._targets_by_range(isolated))
|
||||
return prioritized
|
||||
|
||||
def air_assault_targets(self) -> list[ControlPoint]:
|
||||
"""Returns control points suitable for air assault missions, including neutral bases."""
|
||||
prioritized = []
|
||||
capturable_later = []
|
||||
isolated = []
|
||||
|
||||
combined_control_points = itertools.chain(
|
||||
self.game.theater.control_points_for(self.is_player.opponent),
|
||||
self.game.theater.control_points_for(Player.NEUTRAL),
|
||||
)
|
||||
|
||||
for cp in combined_control_points:
|
||||
if cp.is_isolated:
|
||||
isolated.append(cp)
|
||||
continue
|
||||
|
||||
@@ -43,7 +43,9 @@ class PackageFulfiller:
|
||||
|
||||
@property
|
||||
def is_player(self) -> bool:
|
||||
return self.coalition.player
|
||||
if self.coalition.player.is_blue:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def ato(self) -> AirTaskingOrder:
|
||||
|
||||
@@ -10,7 +10,7 @@ from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
|
||||
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
from game.theater import ControlPoint, FrontLine
|
||||
from game.theater import ControlPoint, FrontLine, Player
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -26,12 +26,12 @@ class CaptureBase(CompoundTask[TheaterState]):
|
||||
def enemy_cp(self, state: TheaterState) -> ControlPoint:
|
||||
return self.front_line.control_point_hostile_to(state.context.coalition.player)
|
||||
|
||||
def units_deployable(self, state: TheaterState, player: bool) -> int:
|
||||
def units_deployable(self, state: TheaterState, player: Player) -> int:
|
||||
cp = self.front_line.control_point_friendly_to(player)
|
||||
ammo_depots = list(state.ammo_dumps_at(cp))
|
||||
return cp.deployable_front_line_units_with(len(ammo_depots))
|
||||
|
||||
def unit_cap(self, state: TheaterState, player: bool) -> int:
|
||||
def unit_cap(self, state: TheaterState, player: Player) -> int:
|
||||
cp = self.front_line.control_point_friendly_to(player)
|
||||
ammo_depots = list(state.ammo_dumps_at(cp))
|
||||
return cp.front_line_capacity_with(len(ammo_depots))
|
||||
|
||||
@@ -11,10 +11,11 @@ from game.theater import FrontLine
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
from game.theater.player import Player
|
||||
|
||||
|
||||
class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
||||
def __init__(self, front_line: FrontLine, player: bool) -> None:
|
||||
def __init__(self, front_line: FrontLine, player: Player) -> None:
|
||||
self.front_line = front_line
|
||||
self.friendly_cp = self.front_line.control_point_friendly_to(player)
|
||||
self.enemy_cp = self.front_line.control_point_hostile_to(player)
|
||||
@@ -26,7 +27,7 @@ class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
||||
@staticmethod
|
||||
def management_allowed(state: TheaterState) -> bool:
|
||||
return (
|
||||
not state.context.coalition.player
|
||||
not state.context.coalition.player.is_blue
|
||||
or state.context.settings.automate_front_line_stance
|
||||
)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if (
|
||||
state.context.coalition.player
|
||||
state.context.coalition.player.is_blue
|
||||
and state.context.settings.auto_ato_behavior is AutoAtoBehavior.Disabled
|
||||
):
|
||||
return False
|
||||
@@ -102,7 +102,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
return 1
|
||||
|
||||
def fulfill_mission(self, state: TheaterState) -> bool:
|
||||
color = "blue" if state.context.coalition.player else "red"
|
||||
color = "blue" if state.context.coalition.player.is_blue else "red"
|
||||
self.propose_flights()
|
||||
fulfiller = PackageFulfiller(
|
||||
state.context.coalition,
|
||||
@@ -214,7 +214,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
settings = state.context.coalition.game.settings
|
||||
margin = 100 - (
|
||||
settings.ownfor_autoplanner_aggressiveness
|
||||
if state.context.coalition.player
|
||||
if state.context.coalition.player.is_blue
|
||||
else settings.opfor_autoplanner_aggressiveness
|
||||
)
|
||||
threat_range = iads_threat.max_threat_range() * (margin / 100)
|
||||
|
||||
@@ -12,7 +12,7 @@ from game.theater import MissionTarget
|
||||
class PlanAewc(PackagePlanningTask[MissionTarget]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if (
|
||||
state.context.coalition.player
|
||||
state.context.coalition.player.is_blue
|
||||
and not state.context.settings.auto_ato_behavior_awacs
|
||||
):
|
||||
return False
|
||||
|
||||
@@ -6,7 +6,7 @@ from game.ato.flighttype import FlightType
|
||||
from game.commander.missionproposals import EscortType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import FrontLine
|
||||
from game.theater import FrontLine, Player
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -19,9 +19,8 @@ class PlanCas(PackagePlanningTask[FrontLine]):
|
||||
# An exception is made for turn zero since that's not being truly planned, but
|
||||
# just to determine what missions should be planned on turn 1 (when there *will*
|
||||
# be ground units) and what aircraft should be ordered.
|
||||
enemy_cp = self.target.control_point_friendly_to(
|
||||
player=not state.context.coalition.player
|
||||
)
|
||||
player = state.context.coalition.player.opponent
|
||||
enemy_cp = self.target.control_point_friendly_to(player)
|
||||
if enemy_cp.deployable_front_line_units == 0 and state.context.turn > 0:
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
@@ -14,7 +14,7 @@ MARGIN = 4 # assume 4 aircraft can land without refueling
|
||||
class PlanRecovery(PackagePlanningTask[ControlPoint]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if (
|
||||
state.context.coalition.player
|
||||
state.context.coalition.player.is_blue
|
||||
and not state.context.settings.auto_ato_behavior_tankers
|
||||
):
|
||||
return False
|
||||
|
||||
@@ -12,7 +12,7 @@ from game.theater import MissionTarget
|
||||
class PlanRefueling(PackagePlanningTask[MissionTarget]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if (
|
||||
state.context.coalition.player
|
||||
state.context.coalition.player.is_blue
|
||||
and not state.context.settings.auto_ato_behavior_tankers
|
||||
):
|
||||
return False
|
||||
|
||||
@@ -56,7 +56,7 @@ https://en.wikipedia.org/wiki/Hierarchical_task_network
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from game.ato.starttype import StartType
|
||||
from game.commander.tasks.compound.nextaction import PlanNextAction
|
||||
@@ -67,10 +67,11 @@ from game.profiling import MultiEventTracer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.theater.player import Player
|
||||
|
||||
|
||||
class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
def __init__(self, game: Game, player: Player) -> None:
|
||||
super().__init__(
|
||||
PlanNextAction(
|
||||
aircraft_cold_start=game.settings.default_start_type is StartType.COLD
|
||||
@@ -79,6 +80,18 @@ class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
|
||||
self.game = game
|
||||
self.player = player
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Migration: Convert old boolean player values to Player enum
|
||||
if "player" in state and isinstance(state["player"], bool):
|
||||
from game.theater.player import Player
|
||||
|
||||
if state["player"]:
|
||||
state["player"] = Player.BLUE
|
||||
else:
|
||||
state["player"] = Player.RED
|
||||
|
||||
self.__dict__.update(state)
|
||||
|
||||
def plan_missions(self, now: datetime, tracer: MultiEventTracer) -> None:
|
||||
state = TheaterState.from_game(self.game, self.player, now, tracer)
|
||||
while True:
|
||||
|
||||
@@ -20,6 +20,7 @@ from game.theater import (
|
||||
ControlPoint,
|
||||
FrontLine,
|
||||
MissionTarget,
|
||||
Player,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
@@ -152,11 +153,12 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
|
||||
@classmethod
|
||||
def from_game(
|
||||
cls, game: Game, player: bool, now: datetime, tracer: MultiEventTracer
|
||||
cls, game: Game, player: Player, now: datetime, tracer: MultiEventTracer
|
||||
) -> TheaterState:
|
||||
coalition = game.coalition_for(player)
|
||||
finder = ObjectiveFinder(game, player)
|
||||
ordered_capturable_points = finder.prioritized_points()
|
||||
air_assault_capturable_points = finder.air_assault_targets()
|
||||
|
||||
context = PersistentContext(
|
||||
game.db,
|
||||
@@ -176,7 +178,7 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
|
||||
battle_postitions: Dict[ControlPoint, BattlePositions] = {
|
||||
cp: BattlePositions.for_control_point(cp)
|
||||
for cp in ordered_capturable_points
|
||||
for cp in air_assault_capturable_points
|
||||
}
|
||||
|
||||
vulnerable_control_points = [
|
||||
@@ -213,8 +215,8 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
)
|
||||
),
|
||||
strike_targets=list(finder.strike_targets()),
|
||||
enemy_barcaps=list(game.theater.control_points_for(not player)),
|
||||
threat_zones=game.threat_zone_for(not player),
|
||||
enemy_barcaps=list(game.theater.control_points_for(player.opponent)),
|
||||
threat_zones=game.threat_zone_for(player.opponent),
|
||||
vulnerable_control_points=vulnerable_control_points,
|
||||
control_point_priority_queue=ordered_capturable_points,
|
||||
priority_cp=(
|
||||
|
||||
65
game/controlpoint_influenceradius.py
Normal file
65
game/controlpoint_influenceradius.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, List, TYPE_CHECKING
|
||||
from functools import cached_property
|
||||
|
||||
from dcs.mapping import Polygon
|
||||
from dcs.triggers import TriggerZone, TriggerZoneCircular, TriggerZoneQuadPoint
|
||||
|
||||
from game.theater.theatergroundobject import NAME_BY_CATEGORY
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dcs.mapping import Point
|
||||
|
||||
|
||||
class ControlPointInfluenceRadiusError(RuntimeError):
|
||||
"""Error for when there are insufficient conditions to create a ControlPointInfluenceRadius."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ControlPointInfluenceRadius:
|
||||
"""Store information about a scenery objective."""
|
||||
|
||||
def __init__(self, zone_def: TriggerZone, cp_name: str) -> None:
|
||||
self.zone_def = zone_def
|
||||
self.position = zone_def.position
|
||||
self.cp_name = cp_name
|
||||
|
||||
@staticmethod
|
||||
def from_trigger_zones(
|
||||
trigger_zones: Iterable[TriggerZone],
|
||||
) -> List[ControlPointInfluenceRadius]:
|
||||
"""Define scenery objectives based on their encompassing blue/red circle."""
|
||||
zone_definitions = []
|
||||
cp_influence_zones = []
|
||||
|
||||
for zone in trigger_zones:
|
||||
if ControlPointInfluenceRadius.is_red(zone):
|
||||
zone_definitions.append(zone)
|
||||
|
||||
# For each objective definition.
|
||||
for zone_def in zone_definitions:
|
||||
if len(zone_def.properties) == 0:
|
||||
raise ControlPointInfluenceRadiusError(
|
||||
"Undefined ControlPointInfluenceRadius category in TriggerZone: "
|
||||
+ zone_def.name
|
||||
)
|
||||
zone_def_cp_name = zone_def.properties[1].get("value")
|
||||
cp_influence_zones.append(
|
||||
ControlPointInfluenceRadius(zone_def, zone_def_cp_name)
|
||||
)
|
||||
return cp_influence_zones
|
||||
|
||||
@staticmethod
|
||||
def is_red(zone: TriggerZone) -> bool:
|
||||
# Red in RGB is [1 Red], [0 Green], [0 Blue]. Ignore the fourth position: Transparency.
|
||||
return zone.color[1] == 1 and zone.color[2] == 0 and zone.color[3] == 0
|
||||
|
||||
|
||||
def point_in_zone(zone: TriggerZone, pos: Point) -> bool:
|
||||
if isinstance(zone, TriggerZoneCircular):
|
||||
return zone.position.distance_to_point(pos) < zone.radius
|
||||
elif isinstance(zone, TriggerZoneQuadPoint):
|
||||
return Polygon(pos._terrain, zone.verticies).point_in_poly(pos)
|
||||
raise RuntimeError(f"Invalid trigger-zone: {zone.name}")
|
||||
@@ -6,6 +6,7 @@ REQUIRED_BUILDINGS = [
|
||||
"ammo",
|
||||
"factory",
|
||||
"fob",
|
||||
"invisiblefob",
|
||||
"oil",
|
||||
]
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ class GroupTask(Enum):
|
||||
FARP = ("Farp", GroupRole.BUILDING)
|
||||
FOB = ("FOB", GroupRole.BUILDING)
|
||||
FUEL = ("Fuel", GroupRole.BUILDING)
|
||||
INVISIBLE_FOB = ("InvisibleFOB", GroupRole.BUILDING)
|
||||
OFFSHORE_STRIKE_TARGET = ("OffShoreStrikeTarget", GroupRole.BUILDING)
|
||||
OIL = ("Oil", GroupRole.BUILDING)
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ class WeaponType(Enum):
|
||||
TGP = "TGP"
|
||||
DECOY = "DECOY"
|
||||
JAMMER = "JAMMER"
|
||||
OFFENSIVE_JAMMER = "OFFENSIVE_JAMMER"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ from game.radio.channels import (
|
||||
WarthogChannelNamer,
|
||||
PhantomChannelNamer,
|
||||
KiowaChannelNamer,
|
||||
ARC5RadioChannelAllocator,
|
||||
ARC5ChannelNamer,
|
||||
FulcrumChannelNamer,
|
||||
)
|
||||
from game.utils import (
|
||||
Distance,
|
||||
@@ -94,6 +97,7 @@ class RadioConfig:
|
||||
return None
|
||||
allocator_type: Type[RadioChannelAllocator] = {
|
||||
"SCR-522": SCR522RadioChannelAllocator,
|
||||
"ARC-5": ARC5RadioChannelAllocator,
|
||||
"common": CommonRadioChannelAllocator,
|
||||
"farmer": FarmerRadioChannelAllocator,
|
||||
"noop": NoOpChannelAllocator,
|
||||
@@ -105,6 +109,7 @@ class RadioConfig:
|
||||
def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]:
|
||||
return {
|
||||
"SCR-522": SCR522ChannelNamer,
|
||||
"ARC-5": ARC5ChannelNamer,
|
||||
"default": ChannelNamer,
|
||||
"huey": HueyChannelNamer,
|
||||
"mirage": MirageChannelNamer,
|
||||
@@ -118,6 +123,7 @@ class RadioConfig:
|
||||
"a10c-ii": WarthogChannelNamer,
|
||||
"phantom": PhantomChannelNamer,
|
||||
"kiowa": KiowaChannelNamer,
|
||||
"fulcrum": FulcrumChannelNamer,
|
||||
}[config.get("namer", "default")]
|
||||
|
||||
|
||||
@@ -229,6 +235,14 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
# when no TGP is mounted on any station.
|
||||
has_built_in_target_pod: bool
|
||||
|
||||
# indicates if the aircraft has a built-in jammer allowing Defensive EWJamming to be used
|
||||
# without the need for a jamming pod
|
||||
has_built_in_ecm: bool
|
||||
|
||||
# Indicates if the aircraft has a built in jammer allowing Offensive EWJamming to be used
|
||||
# without the need for a jamming pod
|
||||
has_built_in_jamming: bool
|
||||
|
||||
task_priorities: dict[FlightType, int]
|
||||
laser_code_configs: list[LaserCodeConfig]
|
||||
|
||||
@@ -591,6 +605,8 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
can_carry_crates=data.get("can_carry_crates", aircraft.helicopter),
|
||||
task_priorities=task_priorities,
|
||||
has_built_in_target_pod=data.get("has_built_in_target_pod", False),
|
||||
has_built_in_ecm=data.get("has_built_in_ecm", False),
|
||||
has_built_in_jamming=data.get("has_built_in_jamming", False),
|
||||
laser_code_configs=[
|
||||
LaserCodeConfig.from_yaml(d) for d in data.get("laser_codes", [])
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ from uuid import UUID
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater import Airfield, ControlPoint
|
||||
from game.theater import Airfield, ControlPoint, Player
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -45,9 +45,9 @@ class AirLosses:
|
||||
def losses(self) -> Iterator[FlyingUnit]:
|
||||
return itertools.chain(self.player, self.enemy)
|
||||
|
||||
def by_type(self, player: bool) -> Dict[AircraftType, int]:
|
||||
def by_type(self, player: Player) -> Dict[AircraftType, int]:
|
||||
losses_by_type: Dict[AircraftType, int] = defaultdict(int)
|
||||
losses = self.player if player else self.enemy
|
||||
losses = self.player if player.is_blue else self.enemy
|
||||
for loss in losses:
|
||||
losses_by_type[loss.flight.unit_type] += 1
|
||||
return losses_by_type
|
||||
@@ -87,7 +87,7 @@ class GroundLosses:
|
||||
@dataclass(frozen=True)
|
||||
class BaseCaptureEvent:
|
||||
control_point: ControlPoint
|
||||
captured_by_player: bool
|
||||
captured_by_player: Player
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -166,7 +166,7 @@ class Debriefing:
|
||||
|
||||
def merge_simulation_results(self, results: SimulationResults) -> None:
|
||||
for air_loss in results.air_losses:
|
||||
if air_loss.flight.squadron.player:
|
||||
if air_loss.flight.squadron.player.is_blue:
|
||||
self.air_losses.player.append(air_loss)
|
||||
else:
|
||||
self.air_losses.enemy.append(air_loss)
|
||||
@@ -209,9 +209,9 @@ class Debriefing:
|
||||
def casualty_count(self, control_point: ControlPoint) -> int:
|
||||
return len([x for x in self.front_line_losses if x.origin == control_point])
|
||||
|
||||
def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
def front_line_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
if player:
|
||||
if player.is_blue:
|
||||
losses = self.ground_losses.player_front_line
|
||||
else:
|
||||
losses = self.ground_losses.enemy_front_line
|
||||
@@ -219,9 +219,9 @@ class Debriefing:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
def convoy_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
if player:
|
||||
if player.is_blue:
|
||||
losses = self.ground_losses.player_convoy
|
||||
else:
|
||||
losses = self.ground_losses.enemy_convoy
|
||||
@@ -229,9 +229,9 @@ class Debriefing:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def cargo_ship_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
def cargo_ship_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
if player:
|
||||
if player.is_blue:
|
||||
ships = self.ground_losses.player_cargo_ships
|
||||
else:
|
||||
ships = self.ground_losses.enemy_cargo_ships
|
||||
@@ -240,9 +240,9 @@ class Debriefing:
|
||||
losses_by_type[unit_type] += count
|
||||
return losses_by_type
|
||||
|
||||
def airlift_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
def airlift_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
if player:
|
||||
if player.is_blue:
|
||||
losses = self.ground_losses.player_airlifts
|
||||
else:
|
||||
losses = self.ground_losses.enemy_airlifts
|
||||
@@ -251,9 +251,9 @@ class Debriefing:
|
||||
losses_by_type[unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def ground_object_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||
def ground_object_losses_by_type(self, player: Player) -> Dict[str, int]:
|
||||
losses_by_type: Dict[str, int] = defaultdict(int)
|
||||
if player:
|
||||
if player.is_blue:
|
||||
losses = self.ground_losses.player_ground_objects
|
||||
else:
|
||||
losses = self.ground_losses.enemy_ground_objects
|
||||
@@ -261,9 +261,9 @@ class Debriefing:
|
||||
losses_by_type[loss.theater_unit.type.id] += 1
|
||||
return losses_by_type
|
||||
|
||||
def scenery_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||
def scenery_losses_by_type(self, player: Player) -> Dict[str, int]:
|
||||
losses_by_type: Dict[str, int] = defaultdict(int)
|
||||
if player:
|
||||
if player.is_blue:
|
||||
losses = self.ground_losses.player_scenery
|
||||
else:
|
||||
losses = self.ground_losses.enemy_scenery
|
||||
@@ -279,7 +279,7 @@ class Debriefing:
|
||||
if aircraft is None:
|
||||
logging.error(f"Could not find Flight matching {unit_name}")
|
||||
continue
|
||||
if aircraft.flight.departure.captured:
|
||||
if aircraft.flight.departure.captured.is_blue:
|
||||
player_losses.append(aircraft)
|
||||
else:
|
||||
enemy_losses.append(aircraft)
|
||||
@@ -290,7 +290,7 @@ class Debriefing:
|
||||
for unit_name in self.state_data.killed_ground_units:
|
||||
front_line_unit = self.unit_map.front_line_unit(unit_name)
|
||||
if front_line_unit is not None:
|
||||
if front_line_unit.origin.captured:
|
||||
if front_line_unit.origin.captured.is_blue:
|
||||
losses.player_front_line.append(front_line_unit)
|
||||
else:
|
||||
losses.enemy_front_line.append(front_line_unit)
|
||||
@@ -298,7 +298,7 @@ class Debriefing:
|
||||
|
||||
convoy_unit = self.unit_map.convoy_unit(unit_name)
|
||||
if convoy_unit is not None:
|
||||
if convoy_unit.convoy.player_owned:
|
||||
if convoy_unit.convoy.player_owned.is_blue:
|
||||
losses.player_convoy.append(convoy_unit)
|
||||
else:
|
||||
losses.enemy_convoy.append(convoy_unit)
|
||||
@@ -306,7 +306,7 @@ class Debriefing:
|
||||
|
||||
cargo_ship = self.unit_map.cargo_ship(unit_name)
|
||||
if cargo_ship is not None:
|
||||
if cargo_ship.player_owned:
|
||||
if cargo_ship.player_owned.is_blue:
|
||||
losses.player_cargo_ships.append(cargo_ship)
|
||||
else:
|
||||
losses.enemy_cargo_ships.append(cargo_ship)
|
||||
@@ -314,7 +314,9 @@ class Debriefing:
|
||||
|
||||
ground_object = self.unit_map.theater_units(unit_name)
|
||||
if ground_object is not None:
|
||||
if ground_object.theater_unit.ground_object.is_friendly(to_player=True):
|
||||
if ground_object.theater_unit.ground_object.is_friendly(
|
||||
to_player=Player.BLUE
|
||||
):
|
||||
losses.player_ground_objects.append(ground_object)
|
||||
else:
|
||||
losses.enemy_ground_objects.append(ground_object)
|
||||
@@ -323,7 +325,9 @@ class Debriefing:
|
||||
scenery_object = self.unit_map.scenery_object(unit_name)
|
||||
# Try appending object to the name, because we do this for building statics.
|
||||
if scenery_object is not None:
|
||||
if scenery_object.ground_unit.ground_object.is_friendly(to_player=True):
|
||||
if scenery_object.ground_unit.ground_object.is_friendly(
|
||||
to_player=Player.BLUE
|
||||
):
|
||||
losses.player_scenery.append(scenery_object)
|
||||
else:
|
||||
losses.enemy_scenery.append(scenery_object)
|
||||
@@ -331,9 +335,9 @@ class Debriefing:
|
||||
|
||||
airfield = self.unit_map.airfield(unit_name)
|
||||
if airfield is not None:
|
||||
if airfield.captured:
|
||||
if airfield.captured.is_blue:
|
||||
losses.player_airfields.append(airfield)
|
||||
else:
|
||||
elif airfield.captured.is_red:
|
||||
losses.enemy_airfields.append(airfield)
|
||||
continue
|
||||
|
||||
@@ -349,7 +353,7 @@ class Debriefing:
|
||||
for unit_name in self.state_data.killed_aircraft:
|
||||
airlift_unit = self.unit_map.airlift_unit(unit_name)
|
||||
if airlift_unit is not None:
|
||||
if airlift_unit.transfer.player:
|
||||
if airlift_unit.transfer.player.is_blue:
|
||||
losses.player_airlifts.append(airlift_unit)
|
||||
else:
|
||||
losses.enemy_airlifts.append(airlift_unit)
|
||||
@@ -377,8 +381,10 @@ class Debriefing:
|
||||
# Captured base is not a part of the campaign. This happens when neutral
|
||||
# bases are near the conflict. Nothing to do.
|
||||
continue
|
||||
|
||||
captured_by_player = int(new_owner_id_str) == blue_coalition_id
|
||||
if int(new_owner_id_str) == blue_coalition_id:
|
||||
captured_by_player = Player.BLUE
|
||||
else:
|
||||
captured_by_player = Player.RED
|
||||
if control_point.is_friendly(to_player=captured_by_player):
|
||||
# Base is currently friendly to the new owner. Was captured and
|
||||
# recaptured in the same mission. Nothing to do.
|
||||
|
||||
@@ -865,6 +865,19 @@ class Faction:
|
||||
# MiG-31BM
|
||||
if not mod_settings.mig31bm_foxhound:
|
||||
self.remove_aircraft("MiG-31BM")
|
||||
# Mlitary Aircraft Mod 1.7.1
|
||||
if not mod_settings.mam:
|
||||
self.remove_aircraft("A400M_Atlas")
|
||||
self.remove_aircraft("B2_Spirit")
|
||||
self.remove_aircraft("C2A_Greyhound")
|
||||
self.remove_aircraft("C5_Galaxy")
|
||||
self.remove_aircraft("KC_10_Extender")
|
||||
self.remove_aircraft("KC_10_Extender_D")
|
||||
self.remove_aircraft("P3C_Orion")
|
||||
self.remove_aircraft("V22_Osprey")
|
||||
# Tornado f.3 ADV (1.0)
|
||||
if not mod_settings.tornado_adv:
|
||||
self.remove_aircraft("Tornado_ADV")
|
||||
|
||||
def remove_aircraft(self, name: str) -> None:
|
||||
for aircraft_set in [self.aircraft, self.awacs, self.tankers]:
|
||||
|
||||
51
game/game.py
51
game/game.py
@@ -4,9 +4,10 @@ import itertools
|
||||
import logging
|
||||
import math
|
||||
from collections.abc import Iterator
|
||||
from copy import deepcopy
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, List, TYPE_CHECKING, Type, Union, cast
|
||||
from typing import Any, List, TYPE_CHECKING, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers
|
||||
@@ -32,7 +33,7 @@ from .infos.information import Information
|
||||
from .lasercodes.lasercoderegistry import LaserCodeRegistry
|
||||
from .profiling import logged_duration
|
||||
from .settings import Settings
|
||||
from .theater import ConflictTheater
|
||||
from .theater import ConflictTheater, Player
|
||||
from .theater.bullseye import Bullseye
|
||||
from .theater.theatergroundobject import (
|
||||
EwrGroundObject,
|
||||
@@ -138,8 +139,11 @@ class Game:
|
||||
self.conditions = self.generate_conditions(forced_time=start_time)
|
||||
|
||||
self.sanitize_sides(player_faction, enemy_faction)
|
||||
self.blue = Coalition(self, player_faction, player_budget, player=True)
|
||||
self.red = Coalition(self, enemy_faction, enemy_budget, player=False)
|
||||
self.blue = Coalition(self, player_faction, player_budget, player=Player.BLUE)
|
||||
self.red = Coalition(self, enemy_faction, enemy_budget, player=Player.RED)
|
||||
neutral_faction = deepcopy(player_faction)
|
||||
neutral_faction.country = self.neutral_country
|
||||
self.neutral = Coalition(self, neutral_faction, 0, player=Player.NEUTRAL)
|
||||
self.blue.set_opponent(self.red)
|
||||
self.red.set_opponent(self.blue)
|
||||
|
||||
@@ -178,10 +182,10 @@ class Game:
|
||||
def point_in_world(self, x: float, y: float) -> Point:
|
||||
return Point(x, y, self.theater.terrain)
|
||||
|
||||
def ato_for(self, player: bool) -> AirTaskingOrder:
|
||||
def ato_for(self, player: Player) -> AirTaskingOrder:
|
||||
return self.coalition_for(player).ato
|
||||
|
||||
def transit_network_for(self, player: bool) -> TransitNetwork:
|
||||
def transit_network_for(self, player: Player) -> TransitNetwork:
|
||||
return self.coalition_for(player).transit_network
|
||||
|
||||
def generate_conditions(self, forced_time: time | None = None) -> Conditions:
|
||||
@@ -209,32 +213,35 @@ class Game:
|
||||
else:
|
||||
enemy_faction.country = country_with_name("Russia")
|
||||
|
||||
def faction_for(self, player: bool) -> Faction:
|
||||
def faction_for(self, player: Player) -> Faction:
|
||||
return self.coalition_for(player).faction
|
||||
|
||||
def faker_for(self, player: bool) -> Faker:
|
||||
def faker_for(self, player: Player) -> Faker:
|
||||
return self.coalition_for(player).faker
|
||||
|
||||
def air_wing_for(self, player: bool) -> AirWing:
|
||||
def air_wing_for(self, player: Player) -> AirWing:
|
||||
return self.coalition_for(player).air_wing
|
||||
|
||||
@property
|
||||
def neutral_country(self) -> Type[Country]:
|
||||
def neutral_country(self) -> Country:
|
||||
"""Return the best fitting country that can be used as neutral faction in the generated mission"""
|
||||
countries_in_use = {self.red.faction.country, self.blue.faction.country}
|
||||
if UnitedNationsPeacekeepers not in countries_in_use:
|
||||
return UnitedNationsPeacekeepers
|
||||
elif Switzerland.name not in countries_in_use:
|
||||
return Switzerland
|
||||
if UnitedNationsPeacekeepers() not in countries_in_use:
|
||||
return UnitedNationsPeacekeepers()
|
||||
elif Switzerland() not in countries_in_use:
|
||||
return Switzerland()
|
||||
else:
|
||||
return USAFAggressors
|
||||
return USAFAggressors()
|
||||
|
||||
def coalition_for(self, player: bool) -> Coalition:
|
||||
if player:
|
||||
def coalition_for(self, player: Player) -> Coalition:
|
||||
if player.is_neutral:
|
||||
return self.neutral
|
||||
elif player.is_blue:
|
||||
return self.blue
|
||||
return self.red
|
||||
else:
|
||||
return self.red
|
||||
|
||||
def adjust_budget(self, amount: float, player: bool) -> None:
|
||||
def adjust_budget(self, amount: float, player: Player) -> None:
|
||||
self.coalition_for(player).adjust_budget(amount)
|
||||
|
||||
def on_load(self, game_still_initializing: bool = False) -> None:
|
||||
@@ -489,7 +496,7 @@ class Game:
|
||||
self.current_group_id += 1
|
||||
return self.current_group_id
|
||||
|
||||
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
|
||||
def compute_transit_network_for(self, player: Player) -> TransitNetwork:
|
||||
return TransitNetworkBuilder(self.theater, player).build()
|
||||
|
||||
def compute_threat_zones(self, events: GameUpdateEvents) -> None:
|
||||
@@ -498,10 +505,10 @@ class Game:
|
||||
self.blue.compute_nav_meshes(events)
|
||||
self.red.compute_nav_meshes(events)
|
||||
|
||||
def threat_zone_for(self, player: bool) -> ThreatZones:
|
||||
def threat_zone_for(self, player: Player) -> ThreatZones:
|
||||
return self.coalition_for(player).threat_zone
|
||||
|
||||
def navmesh_for(self, player: bool) -> NavMesh:
|
||||
def navmesh_for(self, player: Player) -> NavMesh:
|
||||
return self.coalition_for(player).nav_mesh
|
||||
|
||||
def compute_unculled_zones(self, events: GameUpdateEvents) -> None:
|
||||
|
||||
@@ -66,7 +66,7 @@ class GroundUnitOrders:
|
||||
bought_units: dict[GroundUnitType, int] = {}
|
||||
units_needing_transfer: dict[GroundUnitType, int] = {}
|
||||
for unit_type, count in self.units.items():
|
||||
allegiance = "Ally" if self.destination.captured else "Enemy"
|
||||
allegiance = "Ally" if self.destination.captured.is_blue else "Enemy"
|
||||
d: dict[GroundUnitType, int]
|
||||
if self.destination != ground_unit_source:
|
||||
source = ground_unit_source
|
||||
|
||||
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.config import REWARDS
|
||||
from game.theater.player import Player
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -22,8 +23,8 @@ class BuildingIncome:
|
||||
|
||||
|
||||
class Income:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
if player:
|
||||
def __init__(self, game: Game, player: Player) -> None:
|
||||
if player.is_blue:
|
||||
self.multiplier = game.settings.player_income_multiplier
|
||||
else:
|
||||
self.multiplier = game.settings.enemy_income_multiplier
|
||||
|
||||
@@ -288,6 +288,8 @@ class BuildingLayout(TgoLayout):
|
||||
@property
|
||||
def category(self) -> str:
|
||||
for task in self.tasks:
|
||||
if task is GroupTask.INVISIBLE_FOB:
|
||||
return "fob"
|
||||
if task not in [GroupTask.STRIKE_TARGET, GroupTask.OFFSHORE_STRIKE_TARGET]:
|
||||
return task.description.lower()
|
||||
raise RuntimeError(f"Building Template {self.name} has no building category")
|
||||
|
||||
@@ -192,7 +192,8 @@ class AircraftBehavior:
|
||||
|
||||
@staticmethod
|
||||
def configure_eplrs(group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
if flight.unit_type.eplrs_capable:
|
||||
eplrs_enabled = flight.coalition.game.settings.eplrs_enabled
|
||||
if eplrs_enabled and flight.unit_type.eplrs_capable:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
|
||||
def configure_cap(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
@@ -306,7 +307,7 @@ class AircraftBehavior:
|
||||
)
|
||||
|
||||
def configure_oca_strike(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
self.configure_task(flight, group, CAS, [AFAC, SEAD])
|
||||
self.configure_task(flight, group, CAS, [AFAC, SEAD, AntishipStrike])
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
|
||||
@@ -13,6 +13,7 @@ from dcs.datalinks.datalink import DataLinkType
|
||||
from dcs.datalinks.datalinkbase import DataLinkSettingsWithFlightLead
|
||||
from dcs.datalinks.link16 import Link16Network, ViperLink16NetworkMemberLink
|
||||
from dcs.mission import Mission
|
||||
from dcs.task import RunScript
|
||||
from dcs.terrain.terrain import NoParkingSlotError
|
||||
from dcs.triggers import TriggerOnce, Event
|
||||
from dcs.unit import Skill
|
||||
@@ -103,6 +104,23 @@ class AircraftGenerator:
|
||||
for parking_slot in cp.parking_slots:
|
||||
parking_slot.unit_id = None
|
||||
|
||||
def _prioritized_packages(self, ato: AirTaskingOrder) -> List[Package]:
|
||||
"""Returns the packages in the order they should be generated."""
|
||||
return sorted(
|
||||
ato.packages,
|
||||
key=lambda p: (
|
||||
(
|
||||
1
|
||||
if any(
|
||||
f.flight_type in [FlightType.AEWC, FlightType.REFUELING]
|
||||
for f in p.flights
|
||||
)
|
||||
else 0
|
||||
),
|
||||
p.time_over_target,
|
||||
),
|
||||
)
|
||||
|
||||
def generate_flights(
|
||||
self,
|
||||
country: Country,
|
||||
@@ -123,7 +141,7 @@ class AircraftGenerator:
|
||||
self._reserve_frequencies_and_tacan(ato)
|
||||
self.mission_data.packages.clear()
|
||||
|
||||
for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)):
|
||||
for package in reversed(self._prioritized_packages(ato)):
|
||||
logging.info(f"Generating package for target: {package.target.name}")
|
||||
if not package.flights:
|
||||
continue
|
||||
@@ -202,7 +220,7 @@ class AircraftGenerator:
|
||||
):
|
||||
continue
|
||||
|
||||
if control_point.captured:
|
||||
if control_point.captured.is_blue:
|
||||
country = player_country
|
||||
else:
|
||||
country = enemy_country
|
||||
@@ -219,12 +237,12 @@ class AircraftGenerator:
|
||||
squadron.location, Fob
|
||||
)
|
||||
if (
|
||||
squadron.coalition.player
|
||||
squadron.coalition.player.is_blue
|
||||
and self.game.settings.perf_disable_untasked_blufor_aircraft
|
||||
):
|
||||
return
|
||||
elif (
|
||||
not squadron.coalition.player
|
||||
squadron.coalition.player.is_red
|
||||
and self.game.settings.perf_disable_untasked_opfor_aircraft
|
||||
):
|
||||
return
|
||||
@@ -256,7 +274,7 @@ class AircraftGenerator:
|
||||
).create_idle_aircraft()
|
||||
if group:
|
||||
if (
|
||||
not squadron.coalition.player
|
||||
squadron.coalition.player.is_red
|
||||
and squadron.aircraft.flyable
|
||||
and (
|
||||
self.game.settings.enable_squadron_pilot_limits
|
||||
@@ -341,16 +359,51 @@ class AircraftGenerator:
|
||||
def _track_ewrj_flight(self, flight: Flight, group: FlyingGroup[Any]) -> None:
|
||||
if not self.ewrj_package_dict.get(id(flight.package)):
|
||||
self.ewrj_package_dict[id(flight.package)] = []
|
||||
added = False
|
||||
if (
|
||||
flight.package.primary_flight
|
||||
and flight is flight.package.primary_flight
|
||||
or flight.client_count
|
||||
not flight.flight_type.is_air_to_air
|
||||
and any(
|
||||
[
|
||||
wpt
|
||||
for wpt in group.points
|
||||
if wpt.name in ["JOIN", "SPLIT", "RACETRACK START", "RACETRACK END"]
|
||||
and any(
|
||||
[
|
||||
t
|
||||
for t in wpt.tasks
|
||||
if isinstance(t, RunScript)
|
||||
and (
|
||||
"Djamming" in t.params["action"]["params"]["command"]
|
||||
or "EWjamm" in t.params["action"]["params"]["command"]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
and (
|
||||
not self.need_ecm
|
||||
or flight.any_member_has_weapon_of_type(WeaponType.JAMMER)
|
||||
or flight.any_member_has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
|
||||
or flight.squadron.aircraft.has_built_in_ecm
|
||||
or flight.squadron.aircraft.has_built_in_jamming
|
||||
)
|
||||
):
|
||||
self.ewrj_package_dict[id(flight.package)].append(group)
|
||||
added = True
|
||||
if (
|
||||
added
|
||||
or not flight.flight_type.is_air_to_air
|
||||
and self.ewrj_package_dict[id(flight.package)]
|
||||
):
|
||||
for f in flight.package.flights:
|
||||
if f is flight or f.group_id == 0 or f.flight_type.is_air_to_air:
|
||||
continue
|
||||
g = self.mission.find_group_by_id(f.group_id)
|
||||
if (
|
||||
isinstance(g, FlyingGroup)
|
||||
and g not in self.ewrj_package_dict[id(flight.package)]
|
||||
):
|
||||
self.ewrj_package_dict[id(flight.package)].append(g)
|
||||
|
||||
def _reserve_frequencies_and_tacan(self, ato: AirTaskingOrder) -> None:
|
||||
for package in ato.packages:
|
||||
|
||||
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.radio.radios import RadioFrequency
|
||||
from game.runways import RunwayData
|
||||
from game.theater.player import Player
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -42,7 +43,7 @@ class FlightData:
|
||||
size: int
|
||||
|
||||
#: True if this flight belongs to the player's coalition.
|
||||
friendly: bool
|
||||
friendly: Player
|
||||
|
||||
#: Number of seconds after mission start the flight is set to depart.
|
||||
departure_delay: timedelta
|
||||
|
||||
@@ -189,8 +189,17 @@ class FlightGroupConfigurator:
|
||||
return
|
||||
# Check if ecm_required option is enabled
|
||||
jammer_required = settings.plugin_option("ewrj.ecm_required")
|
||||
has_jammer = member.loadout.has_weapon_of_type(WeaponType.JAMMER)
|
||||
if jammer_required and not has_jammer:
|
||||
offensive_jammer = member.loadout.has_weapon_of_type(
|
||||
WeaponType.OFFENSIVE_JAMMER
|
||||
)
|
||||
offensive_inbuilt = self.flight.squadron.aircraft.has_built_in_jamming
|
||||
has_jammer = (
|
||||
member.loadout.has_weapon_of_type(WeaponType.JAMMER) or offensive_jammer
|
||||
)
|
||||
built_in_jammer = (
|
||||
self.flight.squadron.aircraft.has_built_in_ecm or offensive_inbuilt
|
||||
)
|
||||
if jammer_required and not (has_jammer or built_in_jammer):
|
||||
return
|
||||
# Create the original ewrj_menu_trigger for player flight members
|
||||
ewrj_menu_trigger = TriggerStart(comment=f"EWRJ-{unit.name}")
|
||||
@@ -199,6 +208,20 @@ class FlightGroupConfigurator:
|
||||
self.group.points[0].tasks[0] = OptReactOnThreat(
|
||||
OptReactOnThreat.Values.PassiveDefense
|
||||
)
|
||||
# Create LUA Flags for Offensive Jamming in EW Script for Player Flights
|
||||
if not (offensive_jammer or offensive_inbuilt):
|
||||
return
|
||||
ewrj_offensive_trigger = TriggerStart(
|
||||
comment=f"Offensive Jammer Flag {unit.name}"
|
||||
)
|
||||
ewrj_offensive_trigger.add_action(
|
||||
DoScript(
|
||||
String(
|
||||
f'trigger.action.setUserFlag("offensive_jamming_{unit.name}", 1)'
|
||||
)
|
||||
)
|
||||
)
|
||||
self.mission.triggerrules.triggers.append(ewrj_offensive_trigger)
|
||||
|
||||
def setup_radios(self) -> RadioFrequency:
|
||||
freq = self.flight.frequency
|
||||
@@ -276,7 +299,7 @@ class FlightGroupConfigurator:
|
||||
unit.set_player()
|
||||
|
||||
def skill_level_for(self, unit: FlyingUnit, pilot: Optional[Pilot]) -> Skill:
|
||||
if self.flight.squadron.player:
|
||||
if self.flight.squadron.player.is_blue:
|
||||
base_skill = Skill(self.game.settings.player_skill)
|
||||
else:
|
||||
base_skill = Skill(self.game.settings.enemy_skill)
|
||||
|
||||
@@ -9,13 +9,10 @@ from dcs.task import (
|
||||
OptFormation,
|
||||
Targets,
|
||||
SetUnlimitedFuelCommand,
|
||||
RunScript,
|
||||
OptReactOnThreat,
|
||||
)
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.data.weapons import WeaponType
|
||||
from game.theater import NavalControlPoint
|
||||
from game.utils import nautical_miles, feet
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
@@ -63,8 +60,14 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
FlightType.SEAD_SWEEP,
|
||||
FlightType.SEAD,
|
||||
FlightType.SEAD_ESCORT,
|
||||
FlightType.DEAD,
|
||||
]:
|
||||
self.start_defensive_jamming(waypoint)
|
||||
settings = self.flight.coalition.game.settings
|
||||
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
|
||||
if settings.plugins.get("ewrj") and ai_jammer:
|
||||
self.offensive_jamming(waypoint, "start")
|
||||
self.defensive_jamming(waypoint, "start")
|
||||
|
||||
if self.flight.flight_type == FlightType.SEAD_ESCORT:
|
||||
self.handle_sead_escort(doctrine, waypoint)
|
||||
# Let the AI use ECM to preemptively defend themselves.
|
||||
@@ -104,28 +107,6 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
vertical_spacing=doctrine.sead_escort_spacing.feet,
|
||||
)
|
||||
|
||||
def start_defensive_jamming(self, waypoint: MovingPoint) -> None:
|
||||
# Start Defensive Jamming
|
||||
settings = self.flight.coalition.game.settings
|
||||
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
|
||||
if settings.plugins.get("ewrj") and ai_jammer:
|
||||
ecm_required = settings.plugin_option("ewrj.ecm_required")
|
||||
has_jammer = False
|
||||
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
||||
has_jammer = member.loadout.has_weapon_of_type(WeaponType.JAMMER)
|
||||
if ecm_required and not has_jammer:
|
||||
continue
|
||||
if not member.is_player:
|
||||
script_content = f'startDjamming("{unit.name}")'
|
||||
start_jamming_script = RunScript(script_content)
|
||||
waypoint.tasks.append(start_jamming_script)
|
||||
has_jammer = True
|
||||
if has_jammer:
|
||||
passive_defense = OptReactOnThreat(
|
||||
OptReactOnThreat.Values.PassiveDefense
|
||||
)
|
||||
waypoint.tasks.append(passive_defense)
|
||||
|
||||
def configure_escort_tasks(
|
||||
self,
|
||||
waypoint: MovingPoint,
|
||||
|
||||
@@ -13,6 +13,7 @@ from game.ato import Flight, FlightWaypoint
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.ato.starttype import StartType
|
||||
from game.ato.traveltime import GroundSpeed
|
||||
from game.data.weapons import WeaponType
|
||||
from game.missiongenerator.missiondata import MissionData
|
||||
from game.theater import MissionTarget, TheaterUnit, OffMapSpawn
|
||||
|
||||
@@ -157,3 +158,36 @@ class PydcsWaypointBuilder:
|
||||
self.group.units[0].unit_type in (F_14A_135_GR, F_14B)
|
||||
):
|
||||
self.group.add_nav_target_point(self.waypoint.position, "IP")
|
||||
|
||||
def defensive_jamming(self, waypoint: MovingPoint, action: str) -> None:
|
||||
# Explodes incoming missiles within the jamming bubble through the EW-Jamming script
|
||||
settings = self.flight.coalition.game.settings
|
||||
ecm_required = settings.plugin_option("ewrj.ecm_required")
|
||||
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
||||
has_jammer = member.loadout.has_weapon_of_type(
|
||||
WeaponType.JAMMER
|
||||
) or member.loadout.has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
|
||||
built_in_jammer = (
|
||||
self.flight.squadron.aircraft.has_built_in_ecm
|
||||
or self.flight.squadron.aircraft.has_built_in_jamming
|
||||
)
|
||||
if ecm_required and not (has_jammer or built_in_jammer):
|
||||
continue
|
||||
if not member.is_player:
|
||||
script_content = f'{action}IAdefjamming("{unit.name}")'
|
||||
jamming_script = RunScript(script_content)
|
||||
waypoint.tasks.append(jamming_script)
|
||||
|
||||
def offensive_jamming(self, waypoint: MovingPoint, action: str) -> None:
|
||||
# Silences enemy radars through the EW-Jamming script
|
||||
settings = self.flight.coalition.game.settings
|
||||
ecm_required = settings.plugin_option("ewrj.ecm_required")
|
||||
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
||||
has_jammer = member.loadout.has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
|
||||
built_in_jammer = self.flight.squadron.aircraft.has_built_in_jamming
|
||||
if ecm_required and not (has_jammer or built_in_jammer):
|
||||
continue
|
||||
if not member.is_player:
|
||||
script_content = f'{action}EWjamm("{unit.name}")'
|
||||
stop_jamming_script = RunScript(script_content)
|
||||
waypoint.tasks.append(stop_jamming_script)
|
||||
|
||||
@@ -9,8 +9,6 @@ from dcs.task import (
|
||||
Tanker,
|
||||
Targets,
|
||||
SetUnlimitedFuelCommand,
|
||||
RunScript,
|
||||
OptReactOnThreat,
|
||||
)
|
||||
|
||||
from game.ato import FlightType
|
||||
@@ -40,21 +38,12 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
)
|
||||
return
|
||||
|
||||
if self.flight.flight_type == FlightType.AEWC:
|
||||
# Start Offensive Jamming for all AWACS flights
|
||||
settings = self.flight.coalition.game.settings
|
||||
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
|
||||
if settings.plugins.get("ewrj") and ai_jammer:
|
||||
# all units in group are AWACS, no specific checks needed
|
||||
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
||||
script_content = f'startEWjamm("{unit.name}")'
|
||||
start_jamming_script = RunScript(script_content)
|
||||
waypoint.tasks.append(start_jamming_script)
|
||||
|
||||
passive_defense = OptReactOnThreat(
|
||||
OptReactOnThreat.Values.PassiveDefense
|
||||
)
|
||||
waypoint.tasks.append(passive_defense)
|
||||
# Start Defensive Jamming for all flights
|
||||
settings = self.flight.coalition.game.settings
|
||||
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
|
||||
if settings.plugins.get("ewrj") and ai_jammer:
|
||||
self.defensive_jamming(waypoint, "start")
|
||||
self.offensive_jamming(waypoint, "start")
|
||||
|
||||
# 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
|
||||
|
||||
@@ -3,8 +3,6 @@ import logging
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import (
|
||||
SetUnlimitedFuelCommand,
|
||||
RunScript,
|
||||
OptReactOnThreat,
|
||||
)
|
||||
|
||||
from game.ato import FlightType
|
||||
@@ -20,18 +18,12 @@ class RaceTrackEndBuilder(PydcsWaypointBuilder):
|
||||
|
||||
# Disable Offensive Jamming at Racetrack End
|
||||
if self.flight.flight_type == FlightType.AEWC:
|
||||
# Stop Offensive Jamming for all AWACS flights
|
||||
# Stop Defensive Jamming for all AWACS flights
|
||||
settings = self.flight.coalition.game.settings
|
||||
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
|
||||
if settings.plugins.get("ewrj") and ai_jammer:
|
||||
# all units in group are AWACS, no specific checks needed
|
||||
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
||||
script_content = f'stopEWjamming("{unit.name}")'
|
||||
stop_jamming_script = RunScript(script_content)
|
||||
waypoint.tasks.append(stop_jamming_script)
|
||||
|
||||
evade_fire = OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
|
||||
waypoint.tasks.append(evade_fire)
|
||||
self.defensive_jamming(waypoint, "stop")
|
||||
self.offensive_jamming(waypoint, "stop")
|
||||
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
@@ -62,6 +62,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||
# into the SAM instead of waiting for it to come alive
|
||||
engage_task = EngageGroup(miz_group.id)
|
||||
engage_task.params["weaponType"] = DcsWeaponType.ARM.value
|
||||
engage_task.params["expend"] = Expend.All.value
|
||||
waypoint.tasks.append(engage_task)
|
||||
|
||||
# Use other Air-to-Surface Missiles at last
|
||||
|
||||
@@ -5,11 +5,9 @@ from dcs.task import (
|
||||
SetUnlimitedFuelCommand,
|
||||
SwitchWaypoint,
|
||||
RunScript,
|
||||
OptReactOnThreat,
|
||||
)
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.data.weapons import WeaponType
|
||||
from game.utils import knots
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
@@ -49,28 +47,15 @@ class SplitPointBuilder(PydcsWaypointBuilder):
|
||||
FlightType.SEAD_SWEEP,
|
||||
FlightType.SEAD,
|
||||
FlightType.SEAD_ESCORT,
|
||||
FlightType.DEAD,
|
||||
]:
|
||||
if self.flight.flight_type == FlightType.SEAD_ESCORT:
|
||||
# Moved previous escort split tasks
|
||||
if self.flight.flight_type.is_escort_type:
|
||||
index = len(self.group.points)
|
||||
self.group.add_trigger_action(SwitchWaypoint(None, index))
|
||||
self.stop_defensive_jamming(waypoint)
|
||||
|
||||
def stop_defensive_jamming(self, waypoint: MovingPoint) -> None:
|
||||
# Stop Defensive Jamming
|
||||
settings = self.flight.coalition.game.settings
|
||||
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
|
||||
if settings.plugins.get("ewrj") and ai_jammer:
|
||||
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
||||
if settings.plugin_option("ewrj.ecm_required"):
|
||||
ecm = WeaponType.JAMMER
|
||||
if not member.loadout.has_weapon_of_type(ecm):
|
||||
continue
|
||||
if not member.is_player:
|
||||
script_content = f'stopDjamming("{unit.name}")'
|
||||
stop_jamming_script = RunScript(script_content)
|
||||
waypoint.tasks.append(stop_jamming_script)
|
||||
|
||||
evaide_fire = OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
|
||||
waypoint.tasks.append(evaide_fire)
|
||||
settings = self.flight.coalition.game.settings
|
||||
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
|
||||
if settings.plugins.get("ewrj") and ai_jammer:
|
||||
self.offensive_jamming(waypoint, "stop")
|
||||
self.defensive_jamming(waypoint, "stop")
|
||||
|
||||
@@ -17,8 +17,8 @@ from game.radio.radios import RadioFrequency
|
||||
from game.runways import RunwayData
|
||||
from game.theater import ControlPoint, FrontLine
|
||||
from .aircraft.flightdata import FlightData
|
||||
from .missiondata import AwacsInfo, TankerInfo
|
||||
from .flotgenerator import JtacInfo
|
||||
from .missiondata import AwacsInfo, TankerInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -182,7 +182,7 @@ class BriefingGenerator(MissionInfoGenerator):
|
||||
def generate_allied_flights_by_departure(self) -> None:
|
||||
"""Create iterable to display allied flights grouped by departure airfield."""
|
||||
for flight in self.flights:
|
||||
if not flight.client_units and flight.friendly:
|
||||
if not flight.client_units and flight.friendly.is_blue:
|
||||
name = flight.departure.airfield_name
|
||||
if (
|
||||
name in self.allied_flights_by_departure
|
||||
|
||||
@@ -18,6 +18,7 @@ from game.utils import kph
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.theater.player import Player
|
||||
|
||||
|
||||
class ConvoyGenerator:
|
||||
@@ -94,7 +95,7 @@ class ConvoyGenerator:
|
||||
name: str,
|
||||
position: Point,
|
||||
units: dict[GroundUnitType, int],
|
||||
for_player: bool,
|
||||
for_player: Player,
|
||||
) -> VehicleGroup:
|
||||
unit_types = list(units.items())
|
||||
main_unit_type, main_unit_count = unit_types[0]
|
||||
|
||||
@@ -15,6 +15,7 @@ FRONTLINE_COLORS = Rgba(255, 0, 0, 255)
|
||||
WHITE = Rgba(255, 255, 255, 255)
|
||||
CP_RED = Rgba(255, 0, 0, 80)
|
||||
CP_BLUE = Rgba(0, 0, 255, 80)
|
||||
CP_NEUTRAL = Rgba(128, 128, 128, 80)
|
||||
BLUE_PATH_COLOR = Rgba(0, 0, 255, 100)
|
||||
RED_PATH_COLOR = Rgba(255, 0, 0, 100)
|
||||
ACTIVE_PATH_COLOR = Rgba(255, 80, 80, 100)
|
||||
@@ -35,10 +36,12 @@ class DrawingsGenerator:
|
||||
Generate cps as circles
|
||||
"""
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.captured:
|
||||
if cp.captured.is_blue:
|
||||
color = CP_BLUE
|
||||
else:
|
||||
elif cp.captured.is_red:
|
||||
color = CP_RED
|
||||
else:
|
||||
color = CP_NEUTRAL
|
||||
shape = self.player_layer.add_circle(
|
||||
cp.position,
|
||||
TRIGGER_RADIUS_CAPTURE,
|
||||
@@ -61,9 +64,9 @@ class DrawingsGenerator:
|
||||
continue
|
||||
else:
|
||||
# Determine path color
|
||||
if cp.captured and destination.captured:
|
||||
if cp.captured.is_blue and destination.captured.is_blue:
|
||||
color = BLUE_PATH_COLOR
|
||||
elif not cp.captured and not destination.captured:
|
||||
elif cp.captured.is_red and destination.captured.is_red:
|
||||
color = RED_PATH_COLOR
|
||||
else:
|
||||
color = ACTIVE_PATH_COLOR
|
||||
|
||||
@@ -40,7 +40,7 @@ from game.ground_forces.ai_ground_planner import (
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
from game.naming import namegen
|
||||
from game.radio.radios import RadioRegistry
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
from game.theater.controlpoint import ControlPoint, Player
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import Heading
|
||||
from .frontlineconflictdescription import FrontLineConflictDescription
|
||||
@@ -101,12 +101,12 @@ class FlotGenerator:
|
||||
|
||||
# Create player groups at random position
|
||||
player_groups = self._generate_groups(
|
||||
self.player_planned_combat_groups, is_player=True
|
||||
self.player_planned_combat_groups, is_player=Player.BLUE
|
||||
)
|
||||
|
||||
# Create enemy groups at random position
|
||||
enemy_groups = self._generate_groups(
|
||||
self.enemy_planned_combat_groups, is_player=False
|
||||
self.enemy_planned_combat_groups, is_player=Player.RED
|
||||
)
|
||||
|
||||
# TODO: Differentiate AirConflict and GroundConflict classes.
|
||||
@@ -193,7 +193,7 @@ class FlotGenerator:
|
||||
callsign=callsign,
|
||||
region=frontline,
|
||||
code=str(code),
|
||||
blue=True,
|
||||
blue=Player.BLUE,
|
||||
freq=freq,
|
||||
)
|
||||
)
|
||||
@@ -215,7 +215,7 @@ class FlotGenerator:
|
||||
def gen_infantry_group_for_group(
|
||||
self,
|
||||
group: VehicleGroup,
|
||||
is_player: bool,
|
||||
is_player: Player,
|
||||
side: Country,
|
||||
forward_heading: Heading,
|
||||
) -> None:
|
||||
@@ -294,7 +294,7 @@ class FlotGenerator:
|
||||
GroundForcePainter(faction, vehicle).apply_livery()
|
||||
vg.hidden_on_mfd = True
|
||||
|
||||
def _earliest_tot_on_flot(self, player: bool) -> timedelta:
|
||||
def _earliest_tot_on_flot(self, player: Player) -> timedelta:
|
||||
tots = [
|
||||
x.time_over_target
|
||||
for x in self.game.ato_for(player).packages
|
||||
@@ -413,7 +413,7 @@ class FlotGenerator:
|
||||
"""
|
||||
duration = timedelta()
|
||||
if stance in [CombatStance.DEFENSIVE, CombatStance.AGGRESSIVE]:
|
||||
duration = self._earliest_tot_on_flot(not to_cp.coalition.player)
|
||||
duration = self._earliest_tot_on_flot(to_cp.coalition.player.opponent)
|
||||
self._set_reform_waypoint(dcs_group, forward_heading, duration)
|
||||
if stance == CombatStance.AGGRESSIVE:
|
||||
# Attack nearest enemy if any
|
||||
@@ -503,7 +503,7 @@ class FlotGenerator:
|
||||
"""
|
||||
duration = timedelta()
|
||||
if stance in [CombatStance.DEFENSIVE, CombatStance.AGGRESSIVE]:
|
||||
duration = self._earliest_tot_on_flot(not to_cp.coalition.player)
|
||||
duration = self._earliest_tot_on_flot(to_cp.coalition.player.opponent)
|
||||
self._set_reform_waypoint(dcs_group, forward_heading, duration)
|
||||
if stance in [
|
||||
CombatStance.AGGRESSIVE,
|
||||
@@ -762,13 +762,15 @@ class FlotGenerator:
|
||||
)
|
||||
|
||||
def _generate_groups(
|
||||
self, groups: list[CombatGroup], is_player: bool
|
||||
self, groups: list[CombatGroup], is_player: Player
|
||||
) -> List[Tuple[VehicleGroup, CombatGroup]]:
|
||||
"""Finds valid positions for planned groups and generates a pydcs group for them"""
|
||||
positioned_groups = []
|
||||
assert self.conflict.heading is not None
|
||||
spawn_heading = (
|
||||
self.conflict.heading.left if is_player else self.conflict.heading.right
|
||||
self.conflict.heading.left
|
||||
if is_player.is_blue
|
||||
else self.conflict.heading.right
|
||||
)
|
||||
country = self.game.coalition_for(is_player).faction.country
|
||||
country = self.mission.country(country.name)
|
||||
@@ -795,7 +797,7 @@ class FlotGenerator:
|
||||
final_position,
|
||||
heading=spawn_heading.opposite,
|
||||
)
|
||||
if is_player:
|
||||
if is_player == Player.BLUE:
|
||||
g.set_skill(Skill(self.game.settings.player_skill))
|
||||
else:
|
||||
g.set_skill(Skill(self.game.settings.enemy_vehicle_skill))
|
||||
@@ -813,7 +815,7 @@ class FlotGenerator:
|
||||
|
||||
def _generate_group(
|
||||
self,
|
||||
player: bool,
|
||||
player: Player,
|
||||
side: Country,
|
||||
unit_type: GroundUnitType,
|
||||
count: int,
|
||||
|
||||
@@ -10,15 +10,14 @@ from dcs import Mission
|
||||
from dcs.action import DoScript, DoScriptFile
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import TriggerStart
|
||||
from dcs.unit import Skill
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.data.units import UnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.plugins import LuaPluginManager
|
||||
from game.theater import TheaterGroundObject
|
||||
from game.theater.iadsnetwork.iadsrole import IadsRole
|
||||
from game.utils import escape_string_for_lua
|
||||
from game.data.units import UnitClass
|
||||
from .missiondata import MissionData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -102,13 +101,15 @@ class LuaGenerator:
|
||||
for logistic_info in self.mission_data.logistics:
|
||||
if logistic_info.transport not in transports:
|
||||
transports.append(logistic_info.transport)
|
||||
coalition_color = "blue" if logistic_info.blue else "red"
|
||||
coalition_color = "blue" if logistic_info.blue.is_blue else "red"
|
||||
logistics_item = logistics_flights.add_item()
|
||||
logistics_item.add_data_array("pilot_names", logistic_info.pilot_names)
|
||||
logistics_item.add_key_value("pickup_zone", logistic_info.pickup_zone)
|
||||
logistics_item.add_key_value("drop_off_zone", logistic_info.drop_off_zone)
|
||||
logistics_item.add_key_value("target_zone", logistic_info.target_zone)
|
||||
logistics_item.add_key_value("side", str(2 if logistic_info.blue else 1))
|
||||
logistics_item.add_key_value(
|
||||
"side", str(2 if logistic_info.blue.is_blue else 1)
|
||||
)
|
||||
logistics_item.add_key_value("logistic_unit", logistic_info.logistic_unit)
|
||||
logistics_item.add_key_value(
|
||||
"aircraft_type", logistic_info.transport.dcs_id
|
||||
@@ -144,7 +145,7 @@ class LuaGenerator:
|
||||
|
||||
target_points = lua_data.add_item("TargetPoints")
|
||||
for flight in self.mission_data.flights:
|
||||
if flight.friendly and flight.flight_type in [
|
||||
if flight.friendly.is_blue and flight.flight_type in [
|
||||
FlightType.ANTISHIP,
|
||||
FlightType.DEAD,
|
||||
FlightType.SEAD,
|
||||
@@ -178,7 +179,7 @@ class LuaGenerator:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
coalition_object = (
|
||||
lua_data.get_or_create_item("BlueAA")
|
||||
if cp.captured
|
||||
if cp.captured.is_blue
|
||||
else lua_data.get_or_create_item("RedAA")
|
||||
)
|
||||
for ground_object in cp.ground_objects:
|
||||
@@ -202,7 +203,9 @@ class LuaGenerator:
|
||||
# Should probably do the same with all the roles... but the script is already
|
||||
# tolerant of those being empty.
|
||||
for node in self.game.theater.iads_network.skynet_nodes(self.game):
|
||||
coalition = iads_object.get_or_create_item("BLUE" if node.player else "RED")
|
||||
coalition = iads_object.get_or_create_item(
|
||||
"BLUE" if node.player.is_blue else "RED"
|
||||
)
|
||||
iads_type = coalition.get_or_create_item(node.iads_role.value)
|
||||
iads_element = iads_type.add_item()
|
||||
iads_element.add_key_value("dcsGroupName", node.dcs_name)
|
||||
|
||||
@@ -15,6 +15,7 @@ from game.runways import RunwayData
|
||||
if TYPE_CHECKING:
|
||||
from game.radio.radios import RadioFrequency
|
||||
from game.radio.tacan import TacanChannel
|
||||
from game.theater.player import Player
|
||||
from game.utils import Distance
|
||||
from uuid import UUID
|
||||
|
||||
@@ -24,7 +25,7 @@ class GroupInfo:
|
||||
group_name: str
|
||||
callsign: str
|
||||
freq: RadioFrequency
|
||||
blue: bool
|
||||
blue: Player
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -85,7 +86,7 @@ class LogisticsInfo:
|
||||
|
||||
pilot_names: list[str]
|
||||
transport: AircraftType
|
||||
blue: bool
|
||||
blue: Player
|
||||
|
||||
logistic_unit: str = field(default_factory=str)
|
||||
pickup_zone: str = field(default_factory=str)
|
||||
|
||||
@@ -9,6 +9,7 @@ import dcs.lua
|
||||
from dcs import Mission, Point
|
||||
from dcs.coalition import Coalition
|
||||
from dcs.countries import country_dict
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import OptReactOnThreat
|
||||
from dcs.terrain import Airport
|
||||
from dcs.unit import Static
|
||||
@@ -131,28 +132,42 @@ class MissionGenerator:
|
||||
return self.unit_map
|
||||
|
||||
@staticmethod
|
||||
def _configure_ewrj(gen: AircraftGenerator) -> None:
|
||||
def _configure_react_to_threat_for_ew_jamming_packages(
|
||||
gen: AircraftGenerator,
|
||||
) -> None:
|
||||
for groups in gen.ewrj_package_dict.values():
|
||||
optrot = [
|
||||
task
|
||||
for task in groups[0].points[0].tasks
|
||||
if isinstance(task, OptReactOnThreat)
|
||||
][0]
|
||||
assert isinstance(optrot, OptReactOnThreat)
|
||||
if (
|
||||
len(groups) == 1
|
||||
and optrot.value != OptReactOnThreat.Values.PassiveDefense
|
||||
):
|
||||
# primary flight with no EWR-Jamming capability
|
||||
continue
|
||||
for group in groups:
|
||||
tasks = group.points[0].tasks
|
||||
for i in range(len(tasks)):
|
||||
if isinstance(tasks[i], OptReactOnThreat):
|
||||
tasks[i] = OptReactOnThreat(
|
||||
OptReactOnThreat.Values.PassiveDefense
|
||||
)
|
||||
break
|
||||
start_point = [
|
||||
p for p in group.points if p.name in ["JOIN", "RACETRACK START"]
|
||||
]
|
||||
if not start_point:
|
||||
sp = group.points[0]
|
||||
else:
|
||||
sp = start_point[0]
|
||||
MissionGenerator.set_react_to_threat(
|
||||
OptReactOnThreat.Values.PassiveDefense, sp
|
||||
)
|
||||
stop_point = [
|
||||
p for p in group.points if p.name in ["SPLIT", "RACETRACK END"]
|
||||
]
|
||||
if not stop_point:
|
||||
sp = group.points[0]
|
||||
else:
|
||||
sp = stop_point[0]
|
||||
MissionGenerator.set_react_to_threat(
|
||||
OptReactOnThreat.Values.EvadeFire, sp
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def set_react_to_threat(
|
||||
optrot: OptReactOnThreat.Values, start_point: MovingPoint
|
||||
) -> None:
|
||||
tasks = start_point.tasks
|
||||
for i in range(len(tasks)):
|
||||
if isinstance(tasks[i], OptReactOnThreat):
|
||||
tasks[i] = OptReactOnThreat(optrot)
|
||||
return
|
||||
tasks.append(OptReactOnThreat(optrot))
|
||||
|
||||
def setup_mission_coalitions(self) -> None:
|
||||
self.mission.coalition["blue"] = Coalition(
|
||||
@@ -291,7 +306,7 @@ class MissionGenerator:
|
||||
flight.aircraft_type.assign_channels_for_flight(flight, self.mission_data)
|
||||
|
||||
if self.game.settings.plugins.get("ewrj"):
|
||||
self._configure_ewrj(aircraft_generator)
|
||||
self._configure_react_to_threat_for_ew_jamming_packages(aircraft_generator)
|
||||
|
||||
def generate_destroyed_units(self) -> None:
|
||||
"""Add destroyed units to the Mission"""
|
||||
@@ -376,9 +391,12 @@ class MissionGenerator:
|
||||
tmu.theater_unit.position,
|
||||
self.mission.terrain,
|
||||
).dict()
|
||||
warehouse["coalition"] = (
|
||||
"blue" if tmu.theater_unit.ground_object.coalition.player else "red"
|
||||
)
|
||||
if tmu.theater_unit.ground_object.coalition.player.is_neutral:
|
||||
warehouse["coalition"] = "neutral"
|
||||
elif tmu.theater_unit.ground_object.coalition.player.is_blue:
|
||||
warehouse["coalition"] = "blue"
|
||||
else:
|
||||
warehouse["coalition"] = "red"
|
||||
warehouse["dynamicCargo"] = settings.dynamic_cargo
|
||||
if tmu.theater_unit.is_ship or tmu.dcs_unit.category == "Heliports": # type: ignore
|
||||
warehouse["dynamicSpawn"] = settings.dynamic_slots
|
||||
|
||||
@@ -13,6 +13,7 @@ from dcs.vehicles import vehicle_map
|
||||
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.naming import namegen
|
||||
from game.theater import Player
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -25,12 +26,12 @@ class RebellionGenerator:
|
||||
|
||||
def generate(self) -> None:
|
||||
ownfor_country = self.mission.country(
|
||||
self.game.coalition_for(player=True).faction.country.name
|
||||
self.game.coalition_for(player=Player.BLUE).faction.country.name
|
||||
)
|
||||
for rz in self.game.theater.ownfor_rebel_zones:
|
||||
self._generate_rebel_zone(ownfor_country, rz)
|
||||
opfor_country = self.mission.country(
|
||||
self.game.coalition_for(player=False).faction.country.name
|
||||
self.game.coalition_for(player=Player.RED).faction.country.name
|
||||
)
|
||||
for rz in self.game.theater.opfor_rebel_zones:
|
||||
self._generate_rebel_zone(opfor_country, rz)
|
||||
|
||||
@@ -67,6 +67,7 @@ from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
|
||||
from game.runways import RunwayData
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
Player,
|
||||
TheaterGroundObject,
|
||||
TheaterUnit,
|
||||
NavalControlPoint,
|
||||
@@ -386,9 +387,9 @@ class GroundObjectGenerator:
|
||||
)
|
||||
self._register_theater_unit(unit, static_group.units[0])
|
||||
|
||||
@staticmethod
|
||||
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
|
||||
if unit_type.eplrs:
|
||||
def enable_eplrs(self, group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
|
||||
eplrs_enabled = self.game.settings.eplrs_enabled
|
||||
if eplrs_enabled and unit_type.eplrs:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
|
||||
def set_alarm_state(self, group: MovingGroup[Any]) -> None:
|
||||
@@ -408,7 +409,7 @@ class GroundObjectGenerator:
|
||||
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
|
||||
color = (
|
||||
{1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
|
||||
if scenery.ground_object.is_friendly(to_player=True)
|
||||
if scenery.ground_object.is_friendly(to_player=Player.BLUE)
|
||||
else {1: 1, 2: 0.2, 3: 0.2, 4: 0.15}
|
||||
)
|
||||
|
||||
@@ -809,9 +810,12 @@ class HelipadGenerator:
|
||||
# capture triggers
|
||||
pad: BaseFARP
|
||||
neutral_country = self.m.country(self.game.neutral_country.name)
|
||||
country = self.m.country(
|
||||
self.game.coalition_for(self.cp.captured).faction.country.name
|
||||
)
|
||||
if self.cp.captured is Player.NEUTRAL:
|
||||
country = neutral_country
|
||||
else:
|
||||
country = self.m.country(
|
||||
self.game.coalition_for(self.cp.captured).faction.country.name
|
||||
)
|
||||
|
||||
name = f"{self.cp.name} {helipad_type} {i}"
|
||||
logging.info("Generating helipad static : " + name)
|
||||
@@ -862,7 +866,7 @@ class HelipadGenerator:
|
||||
|
||||
if self.game.position_culled(helipad):
|
||||
cull_farp_statics = True
|
||||
if self.cp.coalition.player:
|
||||
if self.cp.coalition.player.is_blue:
|
||||
for package in self.cp.coalition.ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.squadron.location == self.cp:
|
||||
@@ -878,7 +882,12 @@ class HelipadGenerator:
|
||||
pad.position,
|
||||
self.m.terrain,
|
||||
).dict()
|
||||
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
|
||||
if self.cp.coalition.player.is_neutral:
|
||||
warehouse["coalition"] = "neutral"
|
||||
elif self.cp.coalition.player.is_blue:
|
||||
warehouse["coalition"] = "blue"
|
||||
else:
|
||||
warehouse["coalition"] = "red"
|
||||
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
|
||||
self.m.warehouses.warehouses[pad.id] = warehouse
|
||||
|
||||
@@ -989,7 +998,7 @@ class GroundSpawnRoadbaseGenerator:
|
||||
cull_farp_statics = True
|
||||
elif self.game.position_culled(ground_spawn[0]):
|
||||
cull_farp_statics = True
|
||||
if self.cp.coalition.player:
|
||||
if self.cp.coalition.player.is_blue:
|
||||
for package in self.cp.coalition.ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.squadron.location == self.cp:
|
||||
@@ -1005,7 +1014,12 @@ class GroundSpawnRoadbaseGenerator:
|
||||
pad.position,
|
||||
self.m.terrain,
|
||||
).dict()
|
||||
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
|
||||
if self.cp.coalition.player.is_neutral:
|
||||
warehouse["coalition"] = "neutral"
|
||||
elif self.cp.coalition.player.is_blue:
|
||||
warehouse["coalition"] = "blue"
|
||||
else:
|
||||
warehouse["coalition"] = "red"
|
||||
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
|
||||
self.m.warehouses.warehouses[pad.id] = warehouse
|
||||
|
||||
@@ -1131,7 +1145,12 @@ class GroundSpawnLargeGenerator:
|
||||
pad.position,
|
||||
self.m.terrain,
|
||||
).dict()
|
||||
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
|
||||
if self.cp.coalition.player.is_neutral:
|
||||
warehouse["coalition"] = "neutral"
|
||||
elif self.cp.coalition.player.is_blue:
|
||||
warehouse["coalition"] = "blue"
|
||||
else:
|
||||
warehouse["coalition"] = "red"
|
||||
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
|
||||
self.m.warehouses.warehouses[pad.id] = warehouse
|
||||
|
||||
@@ -1258,7 +1277,7 @@ class GroundSpawnGenerator:
|
||||
cull_farp_statics = True
|
||||
elif self.game.position_culled(vtol_pad[0]):
|
||||
cull_farp_statics = True
|
||||
if self.cp.coalition.player:
|
||||
if self.cp.coalition.player.is_blue:
|
||||
for package in self.cp.coalition.ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.squadron.location == self.cp:
|
||||
@@ -1275,7 +1294,12 @@ class GroundSpawnGenerator:
|
||||
pad.position,
|
||||
self.m.terrain,
|
||||
).dict()
|
||||
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
|
||||
if self.cp.coalition.player.is_neutral:
|
||||
warehouse["coalition"] = "neutral"
|
||||
elif self.cp.coalition.player.is_blue:
|
||||
warehouse["coalition"] = "blue"
|
||||
else:
|
||||
warehouse["coalition"] = "red"
|
||||
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
|
||||
self.m.warehouses.warehouses[pad.id] = warehouse
|
||||
|
||||
@@ -1382,7 +1406,11 @@ class TgoGenerator:
|
||||
|
||||
def generate(self) -> None:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
country = self.m.country(cp.coalition.faction.country.name)
|
||||
# Use neutral country for neutral control points
|
||||
if cp.captured is Player.NEUTRAL:
|
||||
country = self.m.country(self.game.neutral_country.name)
|
||||
else:
|
||||
country = self.m.country(cp.coalition.faction.country.name)
|
||||
|
||||
# Generate helipads
|
||||
helipad_gen = HelipadGenerator(
|
||||
|
||||
@@ -12,7 +12,6 @@ from dcs.action import (
|
||||
RemoveSceneObjects,
|
||||
RemoveSceneObjectsMask,
|
||||
SceneryDestructionZone,
|
||||
Smoke,
|
||||
)
|
||||
from dcs.condition import (
|
||||
AllOfCoalitionOutsideZone,
|
||||
@@ -33,6 +32,7 @@ from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
|
||||
PUSH_TRIGGER_SIZE = 3000
|
||||
PUSH_TRIGGER_ACTIVATION_AGL = 25
|
||||
@@ -99,9 +99,12 @@ class TriggerGenerator:
|
||||
raise RuntimeError(
|
||||
f"Could not find {airfield.airport.name} in the mission"
|
||||
)
|
||||
cp_airport.set_coalition(
|
||||
airfield.captured and player_coalition or enemy_coalition
|
||||
)
|
||||
if airfield.captured.is_neutral:
|
||||
cp_airport.set_coalition("neutral")
|
||||
elif airfield.captured.is_blue:
|
||||
cp_airport.set_coalition(player_coalition)
|
||||
elif airfield.captured.is_red:
|
||||
cp_airport.set_coalition(enemy_coalition)
|
||||
|
||||
def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None:
|
||||
"""
|
||||
@@ -138,7 +141,7 @@ class TriggerGenerator:
|
||||
zone = self.mission.triggers.add_triggerzone(
|
||||
location, radius=10, hidden=True, name="MARK"
|
||||
)
|
||||
if cp.captured:
|
||||
if cp.captured.is_blue:
|
||||
name = ground_object.obj_name + " [ALLY]"
|
||||
else:
|
||||
name = ground_object.obj_name + " [ENEMY]"
|
||||
@@ -177,26 +180,57 @@ class TriggerGenerator:
|
||||
# clear_trigger.add_action(MessageToAll(text=String("Enable clear trigger"),))
|
||||
self.mission.triggerrules.triggers.append(enable_clear_trigger)
|
||||
|
||||
def _generate_capture_triggers(
|
||||
self, player_coalition: str, enemy_coalition: str
|
||||
) -> None:
|
||||
def _create_capture_trigger(
|
||||
self,
|
||||
trigger_zone_id: int,
|
||||
flag: int,
|
||||
capturing_coalition: str,
|
||||
losing_coalition: str,
|
||||
capturing_coalition_int: int,
|
||||
cp: ControlPoint,
|
||||
flag_condition_true: bool = False,
|
||||
) -> TriggerCondition:
|
||||
"""Helper method to create a single capture trigger with the given parameters."""
|
||||
trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
|
||||
trigger.add_condition(
|
||||
AllOfCoalitionOutsideZone(
|
||||
losing_coalition, trigger_zone_id, unit_type="GROUND"
|
||||
)
|
||||
)
|
||||
trigger.add_condition(
|
||||
PartOfCoalitionInZone(
|
||||
capturing_coalition, trigger_zone_id, unit_type="GROUND"
|
||||
)
|
||||
)
|
||||
|
||||
if flag_condition_true:
|
||||
trigger.add_condition(FlagIsTrue(flag=flag))
|
||||
trigger.add_action(ClearFlag(flag=flag))
|
||||
else:
|
||||
trigger.add_condition(FlagIsFalse(flag=flag))
|
||||
trigger.add_action(SetFlag(flag=flag))
|
||||
|
||||
script_string = String(
|
||||
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{capturing_coalition_int}||{cp.full_name}"'
|
||||
)
|
||||
trigger.add_action(DoScript(script_string))
|
||||
return trigger
|
||||
|
||||
def _generate_capture_triggers(self) -> None:
|
||||
"""Creates a pair of triggers for each control point of `cls.capture_zone_types`.
|
||||
One for the initial capture of a control point, and one if it is recaptured.
|
||||
Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua`
|
||||
"""
|
||||
for cp in self.game.theater.controlpoints:
|
||||
attacking_coalition = str.lower(cp.captured.opponent.name)
|
||||
defending_coalition = str.lower(cp.captured.opponent.opponent.name)
|
||||
if attacking_coalition == "red":
|
||||
attack_coalition_int = 1 # 1 is the Event int for Red
|
||||
defend_coalition_int = 2 # 2 is the Event int for Blue
|
||||
else:
|
||||
attack_coalition_int = 2
|
||||
defend_coalition_int = 1
|
||||
if isinstance(cp, self.capture_zone_types) and not cp.is_carrier:
|
||||
if cp.captured:
|
||||
attacking_coalition = enemy_coalition
|
||||
attack_coalition_int = 1 # 1 is the Event int for Red
|
||||
defending_coalition = player_coalition
|
||||
defend_coalition_int = 2 # 2 is the Event int for Blue
|
||||
else:
|
||||
attacking_coalition = player_coalition
|
||||
attack_coalition_int = 2
|
||||
defending_coalition = enemy_coalition
|
||||
defend_coalition_int = 1
|
||||
|
||||
trigger_zone = self.mission.triggers.add_triggerzone(
|
||||
cp.position,
|
||||
radius=TRIGGER_RADIUS_CAPTURE,
|
||||
@@ -204,43 +238,48 @@ class TriggerGenerator:
|
||||
name="CAPTURE",
|
||||
)
|
||||
flag = self.get_capture_zone_flag()
|
||||
capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
|
||||
capture_trigger.add_condition(
|
||||
AllOfCoalitionOutsideZone(
|
||||
defending_coalition, trigger_zone.id, unit_type="GROUND"
|
||||
)
|
||||
capture = self._create_capture_trigger(
|
||||
trigger_zone.id,
|
||||
flag,
|
||||
attacking_coalition,
|
||||
defending_coalition,
|
||||
attack_coalition_int,
|
||||
cp,
|
||||
)
|
||||
capture_trigger.add_condition(
|
||||
PartOfCoalitionInZone(
|
||||
attacking_coalition, trigger_zone.id, unit_type="GROUND"
|
||||
)
|
||||
recapture = self._create_capture_trigger(
|
||||
trigger_zone.id,
|
||||
flag,
|
||||
defending_coalition,
|
||||
attacking_coalition,
|
||||
defend_coalition_int,
|
||||
cp,
|
||||
flag_condition_true=True,
|
||||
)
|
||||
capture_trigger.add_condition(FlagIsFalse(flag=flag))
|
||||
script_string = String(
|
||||
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{attack_coalition_int}||{cp.full_name}"'
|
||||
)
|
||||
capture_trigger.add_action(DoScript(script_string))
|
||||
capture_trigger.add_action(SetFlag(flag=flag))
|
||||
self.mission.triggerrules.triggers.append(capture_trigger)
|
||||
self.mission.triggerrules.triggers.extend([capture, recapture])
|
||||
|
||||
recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
|
||||
recapture_trigger.add_condition(
|
||||
AllOfCoalitionOutsideZone(
|
||||
attacking_coalition, trigger_zone.id, unit_type="GROUND"
|
||||
if cp.captured.is_neutral:
|
||||
# In this case attacking_coalition is blue and defending_coalition is red because
|
||||
# neutral control points always have blue set as the opponent
|
||||
red_capture = self._create_capture_trigger(
|
||||
trigger_zone.id,
|
||||
flag,
|
||||
defending_coalition,
|
||||
attacking_coalition,
|
||||
defend_coalition_int,
|
||||
cp,
|
||||
)
|
||||
)
|
||||
recapture_trigger.add_condition(
|
||||
PartOfCoalitionInZone(
|
||||
defending_coalition, trigger_zone.id, unit_type="GROUND"
|
||||
red_recapture = self._create_capture_trigger(
|
||||
trigger_zone.id,
|
||||
flag,
|
||||
defending_coalition,
|
||||
attacking_coalition,
|
||||
defend_coalition_int,
|
||||
cp,
|
||||
flag_condition_true=True,
|
||||
)
|
||||
self.mission.triggerrules.triggers.extend(
|
||||
[red_capture, red_recapture]
|
||||
)
|
||||
)
|
||||
recapture_trigger.add_condition(FlagIsTrue(flag=flag))
|
||||
script_string = String(
|
||||
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{defend_coalition_int}||{cp.full_name}"'
|
||||
)
|
||||
recapture_trigger.add_action(DoScript(script_string))
|
||||
recapture_trigger.add_action(ClearFlag(flag=flag))
|
||||
self.mission.triggerrules.triggers.append(recapture_trigger)
|
||||
|
||||
def generate(self) -> None:
|
||||
player_coalition = "blue"
|
||||
@@ -249,7 +288,7 @@ class TriggerGenerator:
|
||||
self._set_skill(player_coalition, enemy_coalition)
|
||||
self._set_allegiances(player_coalition, enemy_coalition)
|
||||
self._gen_markers()
|
||||
self._generate_capture_triggers(player_coalition, enemy_coalition)
|
||||
self._generate_capture_triggers()
|
||||
if self.game.settings.ground_start_scenery_remove_triggers:
|
||||
try:
|
||||
self._generate_clear_statics_trigger(self.game.scenery_clear_zones)
|
||||
|
||||
@@ -55,7 +55,7 @@ class GameStats:
|
||||
turn_data = GameTurnMetadata()
|
||||
|
||||
for cp in game.theater.controlpoints:
|
||||
if cp.captured:
|
||||
if cp.captured.is_blue:
|
||||
for squadron in cp.squadrons:
|
||||
turn_data.allied_units.aircraft_count += squadron.owned_aircraft
|
||||
turn_data.allied_units.vehicles_count += sum(cp.base.armor.values())
|
||||
|
||||
@@ -36,6 +36,18 @@ class DummyObject:
|
||||
class MigrationUnpickler(pickle.Unpickler):
|
||||
"""Custom unpickler to migrate campaign save-files for when components have been moved"""
|
||||
def find_class(self, module: Any, name: str) -> Any:
|
||||
if name == "Su_30MKA_AG":
|
||||
from pydcs_extensions.su30 import Su_30MKA
|
||||
return Su_30MKA
|
||||
if name == "Su_30MKI_AG":
|
||||
from pydcs_extensions.su30 import Su_30MKI
|
||||
return Su_30MKI
|
||||
if name == "Su_30SM_AG":
|
||||
from pydcs_extensions.su30 import Su_30SM
|
||||
return Su_30SM
|
||||
if name == "Su_30MKM_AG":
|
||||
from pydcs_extensions.su30 import Su_30MKM
|
||||
return Su_30MKM
|
||||
if name == "NightMissions":
|
||||
from game.settings import NightMissions
|
||||
return NightMissions
|
||||
@@ -108,6 +120,16 @@ class MigrationUnpickler(pickle.Unpickler):
|
||||
elif name == "Bas_100":
|
||||
from dcs.terrain.kola.airports import Vuojarvi
|
||||
return Vuojarvi
|
||||
elif name == "Alakourtti":
|
||||
from dcs.terrain.kola.airports import Alakurtti
|
||||
return Alakurtti
|
||||
if module == "dcs.terrain.sinai.airports":
|
||||
if name == "Borj_El_Arab_International_Airport":
|
||||
from dcs.terrain.sinai.airports import Borg_El_Arab_International_Airport
|
||||
return Borg_El_Arab_International_Airport
|
||||
elif name == "Palmahim":
|
||||
from dcs.terrain.sinai.airports import Palmachim
|
||||
return Palmachim
|
||||
if module == "dcs.terrain.syria.airports":
|
||||
if name == "Amman":
|
||||
from dcs.terrain.syria.airports import Marka
|
||||
@@ -119,6 +141,10 @@ class MigrationUnpickler(pickle.Unpickler):
|
||||
"Helipad_218"
|
||||
]:
|
||||
return dcs.terrain.Airport # use base-class if airport was removed
|
||||
if module == "dcs.terrain.afghanistan.airports":
|
||||
if name == "Khost_Heliport":
|
||||
from dcs.terrain.afghanistan.airports import FOB_Salerno
|
||||
return FOB_Salerno
|
||||
if module == "dcs.terrain.falklands.airports":
|
||||
if name == "Aerodromo_De_Tolhuin":
|
||||
from dcs.terrain.falklands.airports import Tolhuin
|
||||
|
||||
@@ -35,6 +35,7 @@ from game.runways import RunwayData
|
||||
from game.settings import Settings
|
||||
from game.squadrons import AirWing
|
||||
from game.squadrons import Squadron
|
||||
from game.theater.player import Player
|
||||
from game.theater.controlpoint import (
|
||||
ControlPoint,
|
||||
OffMapSpawn,
|
||||
@@ -833,7 +834,7 @@ class PretenseAircraftGenerator:
|
||||
"""
|
||||
self.initialize_pretense_data_structures(cp)
|
||||
|
||||
is_player = True
|
||||
is_player = Player.BLUE
|
||||
if country == cp.coalition.faction.country:
|
||||
offmap_transport_cp = self.find_pretense_cargo_plane_cp(cp)
|
||||
|
||||
@@ -868,7 +869,7 @@ class PretenseAircraftGenerator:
|
||||
coalition = (
|
||||
self.game.coalition_for(is_player)
|
||||
if country == self.game.coalition_for(is_player).faction.country
|
||||
else self.game.coalition_for(False)
|
||||
else self.game.coalition_for(Player.RED)
|
||||
)
|
||||
self.generate_pretense_aircraft_for_other_side(cp, coalition, ato)
|
||||
|
||||
@@ -1057,6 +1058,7 @@ class PretenseAircraftGenerator:
|
||||
and (
|
||||
not self.need_ecm
|
||||
or flight.any_member_has_weapon_of_type(WeaponType.JAMMER)
|
||||
or flight.any_member_has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
|
||||
)
|
||||
):
|
||||
self.ewrj_package_dict[id(flight.package)].append(group)
|
||||
|
||||
@@ -25,7 +25,7 @@ from game.missiongenerator.aircraft.flightgroupspawner import (
|
||||
)
|
||||
from game.missiongenerator.missiondata import MissionData
|
||||
from game.naming import NameGenerator
|
||||
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint
|
||||
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, Player
|
||||
|
||||
|
||||
class PretenseNameGenerator(NameGenerator):
|
||||
@@ -87,7 +87,7 @@ class PretenseFlightGroupSpawner(FlightGroupSpawner):
|
||||
|
||||
def insert_into_pretense(self, name: str) -> None:
|
||||
cp = self.flight.departure
|
||||
is_player = True
|
||||
is_player = Player.BLUE
|
||||
cp_side = (
|
||||
2
|
||||
if self.flight.coalition
|
||||
|
||||
@@ -1495,13 +1495,13 @@ class PretenseLuaGenerator(LuaGenerator):
|
||||
cp_name.replace("ä", "a")
|
||||
cp_name.replace("ö", "o")
|
||||
cp_name.replace("ø", "o")
|
||||
cp_side = 2 if cp.captured else 1
|
||||
cp_side = 2 if cp.captured.is_blue else 1
|
||||
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
continue
|
||||
elif (
|
||||
cp.is_fleet
|
||||
and cp.captured
|
||||
and cp.captured.is_blue
|
||||
and self.game.settings.pretense_controllable_carrier
|
||||
):
|
||||
# Friendly carrier, generate carrier parameters
|
||||
@@ -1591,7 +1591,7 @@ class PretenseLuaGenerator(LuaGenerator):
|
||||
# Also connect carrier and LHA control points to adjacent friendly points
|
||||
if cp.is_fleet and (
|
||||
not self.game.settings.pretense_controllable_carrier
|
||||
or not cp.captured
|
||||
or cp.captured.is_red
|
||||
):
|
||||
num_of_carrier_connections = 0
|
||||
for (
|
||||
@@ -1616,13 +1616,13 @@ class PretenseLuaGenerator(LuaGenerator):
|
||||
try:
|
||||
if (
|
||||
cp.is_fleet
|
||||
and cp.captured
|
||||
and cp.captured.is_blue
|
||||
and self.game.settings.pretense_controllable_carrier
|
||||
):
|
||||
break
|
||||
elif (
|
||||
closest_cps[extra_connection].is_fleet
|
||||
and closest_cps[extra_connection].captured
|
||||
and closest_cps[extra_connection].captured.is_blue
|
||||
and self.game.settings.pretense_controllable_carrier
|
||||
):
|
||||
break
|
||||
@@ -1647,7 +1647,7 @@ class PretenseLuaGenerator(LuaGenerator):
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
continue
|
||||
cp_side_captured = cp_side == 2
|
||||
if cp_side_captured != cp.captured:
|
||||
if cp_side_captured != cp.captured.is_blue:
|
||||
continue
|
||||
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
|
||||
cp.name
|
||||
@@ -1723,6 +1723,13 @@ class PretenseLuaGenerator(LuaGenerator):
|
||||
def inject_plugin_script(
|
||||
self, plugin_mnemonic: str, script: str, script_mnemonic: str
|
||||
) -> None:
|
||||
# Hard block MOOSE injection for Pretense missions
|
||||
if script_mnemonic.lower() == "moose" or "moose" in script.lower():
|
||||
logging.info(
|
||||
"PretenseLuaGenerator: Skipping hard-blocked Moose.lua injection"
|
||||
)
|
||||
return
|
||||
|
||||
if script_mnemonic in self.plugin_scripts:
|
||||
logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
|
||||
return
|
||||
@@ -1730,7 +1737,6 @@ class PretenseLuaGenerator(LuaGenerator):
|
||||
self.plugin_scripts.append(script_mnemonic)
|
||||
|
||||
plugin_path = Path("./resources/plugins", plugin_mnemonic)
|
||||
|
||||
script_path = Path(plugin_path, script)
|
||||
if not script_path.exists():
|
||||
logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}")
|
||||
|
||||
@@ -28,6 +28,7 @@ from game.missiongenerator.visualsgenerator import VisualsGenerator
|
||||
from game.naming import namegen
|
||||
from game.persistency import pre_pretense_backups_dir
|
||||
from game.pretense.pretenseaircraftgenerator import PretenseAircraftGenerator
|
||||
from game.theater import Player
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.unitmap import UnitMap
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
@@ -233,7 +234,7 @@ class PretenseMissionGenerator(MissionGenerator):
|
||||
callsign=callsign,
|
||||
region=frontline,
|
||||
code=str(code),
|
||||
blue=True,
|
||||
blue=Player.BLUE,
|
||||
freq=freq,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -49,6 +49,7 @@ from game.theater import (
|
||||
TheaterUnit,
|
||||
NavalControlPoint,
|
||||
PresetLocation,
|
||||
Player,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
CarrierGroundObject,
|
||||
@@ -248,7 +249,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator):
|
||||
"""
|
||||
unit_type = None
|
||||
faction = self.coalition.faction
|
||||
is_player = True
|
||||
is_player = Player.BLUE
|
||||
side = (
|
||||
2
|
||||
if self.country == self.game.coalition_for(is_player).faction.country
|
||||
@@ -458,7 +459,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator):
|
||||
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
|
||||
control_point.name
|
||||
)
|
||||
is_player = True
|
||||
is_player = Player.BLUE
|
||||
side = (
|
||||
2
|
||||
if self.country
|
||||
@@ -567,7 +568,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator):
|
||||
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
|
||||
control_point.name
|
||||
)
|
||||
is_player = True
|
||||
is_player = Player.BLUE
|
||||
side = (
|
||||
2
|
||||
if self.country == self.game.coalition_for(is_player).faction.country
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from typing import TYPE_CHECKING, List
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
from dcs.action import (
|
||||
@@ -11,10 +9,6 @@ from dcs.action import (
|
||||
DoScript,
|
||||
MarkToAll,
|
||||
SetFlag,
|
||||
RemoveSceneObjects,
|
||||
RemoveSceneObjectsMask,
|
||||
SceneryDestructionZone,
|
||||
Smoke,
|
||||
)
|
||||
from dcs.condition import (
|
||||
AllOfCoalitionOutsideZone,
|
||||
@@ -22,7 +16,6 @@ from dcs.condition import (
|
||||
FlagIsTrue,
|
||||
PartOfCoalitionInZone,
|
||||
TimeAfter,
|
||||
TimeSinceFlag,
|
||||
)
|
||||
from dcs.mission import Mission
|
||||
from dcs.task import Option
|
||||
@@ -31,12 +24,11 @@ from dcs.terrain.syria.airports import Damascus, Khalkhalah
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import Event, TriggerCondition, TriggerOnce
|
||||
from dcs.unit import Skill
|
||||
from numpy import cross, einsum, arctan2
|
||||
from shapely import MultiPolygon, Point as ShapelyPoint
|
||||
|
||||
from game.naming import ALPHA_MILITARY
|
||||
from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator
|
||||
from game.theater import Airfield
|
||||
from game.theater import Airfield, Player
|
||||
from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE, OffMapSpawn
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -119,7 +111,7 @@ class PretenseTriggerGenerator:
|
||||
f"Could not find {airfield.airport.name} in the mission"
|
||||
)
|
||||
cp_airport.set_coalition(
|
||||
airfield.captured and player_coalition or enemy_coalition
|
||||
airfield.captured.is_blue and player_coalition or enemy_coalition
|
||||
)
|
||||
|
||||
def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None:
|
||||
@@ -157,7 +149,7 @@ class PretenseTriggerGenerator:
|
||||
zone = self.mission.triggers.add_triggerzone(
|
||||
location, radius=10, hidden=True, name="MARK"
|
||||
)
|
||||
if cp.captured:
|
||||
if cp.captured.is_blue:
|
||||
name = ground_object.obj_name + " [ALLY]"
|
||||
else:
|
||||
name = ground_object.obj_name + " [ENEMY]"
|
||||
@@ -174,7 +166,7 @@ class PretenseTriggerGenerator:
|
||||
"""
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if isinstance(cp, self.capture_zone_types) and not cp.is_carrier:
|
||||
if cp.captured:
|
||||
if cp.captured.is_blue:
|
||||
attacking_coalition = enemy_coalition
|
||||
attack_coalition_int = 1 # 1 is the Event int for Red
|
||||
defending_coalition = player_coalition
|
||||
@@ -243,7 +235,7 @@ class PretenseTriggerGenerator:
|
||||
self.game.settings.pretense_carrier_zones_navmesh == "Blue navmesh"
|
||||
)
|
||||
sea_zones_landmap = self.game.coalition_for(
|
||||
player=False
|
||||
player=Player.RED
|
||||
).nav_mesh.theater.landmap
|
||||
if (
|
||||
self.game.settings.pretense_controllable_carrier
|
||||
@@ -251,7 +243,7 @@ class PretenseTriggerGenerator:
|
||||
):
|
||||
navmesh_number = 0
|
||||
for navmesh_poly in self.game.coalition_for(
|
||||
player=use_blue_navmesh
|
||||
player=Player.BLUE if use_blue_navmesh else Player.RED
|
||||
).nav_mesh.polys:
|
||||
navmesh_number += 1
|
||||
if sea_zones_landmap.sea_zones.intersects(navmesh_poly.poly):
|
||||
@@ -325,7 +317,7 @@ class PretenseTriggerGenerator:
|
||||
if (
|
||||
cp.is_fleet
|
||||
and self.game.settings.pretense_controllable_carrier
|
||||
and cp.captured
|
||||
and cp.captured.is_blue
|
||||
):
|
||||
# Friendly carrier zones are generated above
|
||||
continue
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||
from game.config import RUNWAY_REPAIR_COST
|
||||
from game.data.units import UnitClass
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater import ControlPoint, MissionTarget, ParkingType
|
||||
from game.theater import ControlPoint, MissionTarget, ParkingType, Player
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -34,20 +34,20 @@ class ProcurementAi:
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
for_player: bool,
|
||||
owner: Player,
|
||||
faction: Faction,
|
||||
manage_runways: bool,
|
||||
manage_front_line: bool,
|
||||
manage_aircraft: bool,
|
||||
) -> None:
|
||||
self.game = game
|
||||
self.is_player = for_player
|
||||
self.air_wing = game.air_wing_for(for_player)
|
||||
self.is_player = owner
|
||||
self.air_wing = game.air_wing_for(owner)
|
||||
self.faction = faction
|
||||
self.manage_runways = manage_runways
|
||||
self.manage_front_line = manage_front_line
|
||||
self.manage_aircraft = manage_aircraft
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
self.threat_zones = self.game.threat_zone_for(self.is_player.opponent)
|
||||
|
||||
def calculate_ground_unit_budget_share(self) -> float:
|
||||
armor_investment = 0
|
||||
@@ -78,7 +78,7 @@ class ProcurementAi:
|
||||
|
||||
balance = (
|
||||
self.game.settings.auto_procurement_balance
|
||||
if self.is_player
|
||||
if self.is_player.is_blue
|
||||
else self.game.settings.auto_procurement_balance_red
|
||||
)
|
||||
air = balance / 100.0
|
||||
@@ -114,7 +114,7 @@ class ProcurementAi:
|
||||
if control_point.runway_can_be_repaired:
|
||||
control_point.begin_runway_repair()
|
||||
budget -= RUNWAY_REPAIR_COST
|
||||
if self.is_player:
|
||||
if self.is_player.is_blue:
|
||||
self.game.message(
|
||||
"We have begun repairing the runway at " f"{control_point}"
|
||||
)
|
||||
@@ -223,7 +223,7 @@ class ProcurementAi:
|
||||
|
||||
@property
|
||||
def owned_points(self) -> List[ControlPoint]:
|
||||
if self.is_player:
|
||||
if self.is_player.is_blue:
|
||||
return self.game.theater.player_points()
|
||||
else:
|
||||
return self.game.theater.enemy_points()
|
||||
@@ -269,7 +269,7 @@ class ProcurementAi:
|
||||
|
||||
reserves_factor = (
|
||||
self.game.settings.frontline_reserves_factor
|
||||
if self.is_player
|
||||
if self.is_player.is_blue
|
||||
else self.game.settings.frontline_reserves_factor_red
|
||||
)
|
||||
fr_factor = reserves_factor / 100.0
|
||||
@@ -308,7 +308,7 @@ class ProcurementAi:
|
||||
)
|
||||
target = (
|
||||
self.game.settings.reserves_procurement_target
|
||||
if self.is_player
|
||||
if self.is_player.is_blue
|
||||
else self.game.settings.reserves_procurement_target_red
|
||||
)
|
||||
if allocated.total >= target:
|
||||
|
||||
@@ -219,6 +219,27 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator):
|
||||
return "SCR-522"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ARC5RadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the ARC-5 WW2 radios. (4 channels)"""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, mission_data: MissionData
|
||||
) -> None:
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
if flight.departure.atc is not None:
|
||||
flight.assign_channel(radio_id, 2, flight.departure.atc)
|
||||
if flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, 3, flight.arrival.atc)
|
||||
|
||||
# TODO : Some GCI on Channel 4 ?
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "ARC-5"
|
||||
|
||||
|
||||
class ChannelNamer:
|
||||
"""Base class allowing channel name customization per-aircraft.
|
||||
|
||||
@@ -376,6 +397,23 @@ class SCR522ChannelNamer(ChannelNamer):
|
||||
return "SCR-522"
|
||||
|
||||
|
||||
class ARC5ChannelNamer(ChannelNamer):
|
||||
"""
|
||||
Channel namer for F4U-1D Corsair
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
if channel_id > 3:
|
||||
return "?"
|
||||
else:
|
||||
return f"Channel " + "1234"[channel_id - 1]
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "ARC-5"
|
||||
|
||||
|
||||
class LegacyWarthogChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the legacy A-10C."""
|
||||
|
||||
@@ -434,3 +472,15 @@ class KiowaChannelNamer(ChannelNamer):
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "kiowa"
|
||||
|
||||
|
||||
class FulcrumChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the MiG-29 Fulcrum A"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"Ch {channel_id -1}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "fulcrum"
|
||||
|
||||
@@ -84,6 +84,10 @@ def kHz(
|
||||
return RadioFrequency(num * 1000 + hz, modulation)
|
||||
|
||||
|
||||
def Hz(num: int, hz: int = 0, modulation: Modulation = Modulation.AM) -> RadioFrequency:
|
||||
return RadioFrequency(num + hz, modulation)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RadioRange:
|
||||
"""Defines the minimum (inclusive) and maximum (exclusive) range of the radio."""
|
||||
@@ -231,6 +235,15 @@ RADIOS: List[Radio] = [
|
||||
# P-51 / P-47 Radio
|
||||
# 4 preset channels (A/B/C/D)
|
||||
Radio("SCR522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
|
||||
# F4U-1D Radio
|
||||
# 4 preset channels (1/2/3/4)
|
||||
Radio(
|
||||
"ARC-5",
|
||||
(
|
||||
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.AM),
|
||||
RadioRange(MHz(220), MHz(390), kHz(25), Modulation.AM),
|
||||
),
|
||||
),
|
||||
# JF-17 Radios should use AM
|
||||
Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), kHz(25), Modulation.AM),)),
|
||||
Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
|
||||
@@ -343,6 +356,20 @@ RADIOS: List[Radio] = [
|
||||
"AN/ARC-27",
|
||||
(RadioRange(MHz(225), MHz(400), kHz(100), Modulation.AM),),
|
||||
),
|
||||
# MiG-29 Fulcrum A
|
||||
Radio(
|
||||
"R-862",
|
||||
(
|
||||
RadioRange(MHz(220), MHz(400), kHz(25), Modulation.AM),
|
||||
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.AM),
|
||||
RadioRange(MHz(220), MHz(400), kHz(25), Modulation.FM),
|
||||
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.FM),
|
||||
),
|
||||
),
|
||||
Radio(
|
||||
"ARK-19",
|
||||
(RadioRange(kHz(150), kHz(1300), Hz(500), Modulation.AM),),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -29,12 +29,16 @@ class ControlPointJs(BaseModel):
|
||||
destination = None
|
||||
if control_point.target_position is not None:
|
||||
destination = control_point.target_position.latlng()
|
||||
if control_point.captured.is_blue:
|
||||
blue = True
|
||||
else:
|
||||
blue = False
|
||||
return ControlPointJs(
|
||||
id=control_point.id,
|
||||
name=control_point.name,
|
||||
blue=control_point.captured,
|
||||
blue=blue,
|
||||
position=control_point.position.latlng(),
|
||||
mobile=control_point.moveable and control_point.captured,
|
||||
mobile=control_point.moveable and control_point.captured.is_blue,
|
||||
destination=destination,
|
||||
sidc=str(control_point.sidc()),
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, status
|
||||
from starlette.responses import Response
|
||||
|
||||
from game import Game
|
||||
from game.theater.player import Player
|
||||
from .models import ControlPointJs
|
||||
from ..dependencies import GameContext
|
||||
from ..leaflet import LeafletPoint
|
||||
@@ -75,7 +76,7 @@ def set_destination(
|
||||
)
|
||||
if not cp.moveable:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=f"{cp} is not mobile")
|
||||
if not cp.captured:
|
||||
if not cp.captured.is_blue:
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=f"{cp} is not owned by the player"
|
||||
)
|
||||
@@ -120,7 +121,7 @@ def cancel_travel(cp_id: UUID, game: Game = Depends(GameContext.require)) -> Non
|
||||
)
|
||||
if not cp.moveable:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=f"{cp} is not mobile")
|
||||
if not cp.captured:
|
||||
if not cp.captured.is_blue:
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=f"{cp} is not owned by the player"
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ from game.server.mapzones.models import ThreatZonesJs, UnculledZoneJs
|
||||
from game.server.navmesh.models import NavMeshJs
|
||||
from game.server.supplyroutes.models import SupplyRouteJs
|
||||
from game.server.tgos.models import TgoJs
|
||||
from game.theater import Player
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -26,9 +27,9 @@ class GameUpdateEventsJs(BaseModel):
|
||||
new_combats: list[FrozenCombatJs]
|
||||
updated_combats: list[FrozenCombatJs]
|
||||
ended_combats: list[UUID]
|
||||
navmesh_updates: dict[bool, NavMeshJs]
|
||||
navmesh_updates: dict[Player, NavMeshJs]
|
||||
updated_unculled_zones: list[UnculledZoneJs]
|
||||
threat_zones_updated: dict[bool, ThreatZonesJs]
|
||||
threat_zones_updated: dict[Player, ThreatZonesJs]
|
||||
new_flights: list[FlightJs]
|
||||
updated_flights: list[FlightJs]
|
||||
deleted_flights: set[UUID]
|
||||
|
||||
@@ -41,9 +41,13 @@ class FlightJs(BaseModel):
|
||||
waypoints = None
|
||||
if with_waypoints:
|
||||
waypoints = waypoints_for_flight(flight)
|
||||
if flight.blue.is_blue:
|
||||
blue = True
|
||||
else:
|
||||
blue = False
|
||||
return FlightJs(
|
||||
id=flight.id,
|
||||
blue=flight.blue,
|
||||
blue=blue,
|
||||
position=position,
|
||||
sidc=str(flight.sidc()),
|
||||
waypoints=waypoints,
|
||||
|
||||
@@ -49,7 +49,9 @@ class GameJs(BaseModel):
|
||||
iads_network=IadsNetworkJs.from_network(game.theater.iads_network),
|
||||
threat_zones=ThreatZoneContainerJs.for_game(game),
|
||||
navmeshes=NavMeshesJs.from_game(game),
|
||||
map_center=game.theater.terrain.map_view_default.position.latlng(),
|
||||
map_center=LeafletPoint.from_latlng(
|
||||
game.theater.terrain.map_view_default.position.latlng()
|
||||
),
|
||||
unculled_zones=UnculledZoneJs.from_game(game),
|
||||
map_zones=MapZonesJs.from_game(game),
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.server.leaflet import LeafletPoint
|
||||
from game.theater.player import Player
|
||||
from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode, IadsNetwork
|
||||
|
||||
|
||||
@@ -34,8 +35,16 @@ class IadsConnectionJs(BaseModel):
|
||||
iads_connections = []
|
||||
tgo = network_node.group.ground_object
|
||||
for id, connection in network_node.connections.items():
|
||||
if connection.ground_object.is_friendly(True) != tgo.is_friendly(True):
|
||||
if connection.ground_object.is_friendly(Player.BLUE) != tgo.is_friendly(
|
||||
Player.BLUE
|
||||
):
|
||||
continue # Skip connections which are not from same coalition
|
||||
if tgo.is_friendly(Player.BLUE):
|
||||
blue = True
|
||||
elif tgo.is_friendly(Player.RED):
|
||||
blue = False
|
||||
else:
|
||||
continue # Skip neutral
|
||||
iads_connections.append(
|
||||
IadsConnectionJs(
|
||||
id=id,
|
||||
@@ -49,7 +58,7 @@ class IadsConnectionJs(BaseModel):
|
||||
network_node.group.alive_units > 0
|
||||
and connection.alive_units > 0
|
||||
),
|
||||
blue=tgo.is_friendly(True),
|
||||
blue=blue,
|
||||
is_power="power"
|
||||
in [tgo.category, connection.ground_object.category],
|
||||
)
|
||||
|
||||
@@ -19,6 +19,10 @@ class LeafletPoint(BaseModel):
|
||||
|
||||
title = "LatLng"
|
||||
|
||||
@staticmethod
|
||||
def from_latlng(latlng: LatLng) -> LeafletPoint:
|
||||
return LeafletPoint(lat=latlng.lat, lng=latlng.lng)
|
||||
|
||||
|
||||
LeafletLine = list[LeafletPoint]
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.server.leaflet import LeafletPoint, LeafletPoly, ShapelyUtil
|
||||
from game.theater import ConflictTheater
|
||||
from game.theater import ConflictTheater, Player
|
||||
from game.threatzones import ThreatZones
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -85,9 +85,9 @@ class ThreatZoneContainerJs(BaseModel):
|
||||
def for_game(game: Game) -> ThreatZoneContainerJs:
|
||||
return ThreatZoneContainerJs(
|
||||
blue=ThreatZonesJs.from_zones(
|
||||
game.threat_zone_for(player=True), game.theater
|
||||
game.threat_zone_for(player=Player.BLUE), game.theater
|
||||
),
|
||||
red=ThreatZonesJs.from_zones(
|
||||
game.threat_zone_for(player=False), game.theater
|
||||
game.threat_zone_for(player=Player.RED), game.theater
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2,12 +2,13 @@ from fastapi import APIRouter, Depends
|
||||
|
||||
from game import Game
|
||||
from game.server import GameContext
|
||||
from game.theater.player import Player
|
||||
from .models import NavMeshJs
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/navmesh")
|
||||
|
||||
|
||||
@router.get("/", operation_id="get_navmesh", response_model=NavMeshJs)
|
||||
def get(for_player: bool, game: Game = Depends(GameContext.require)) -> NavMeshJs:
|
||||
def get(for_player: Player, game: Game = Depends(GameContext.require)) -> NavMeshJs:
|
||||
mesh = game.coalition_for(for_player).nav_mesh
|
||||
return NavMeshJs.from_navmesh(mesh, game)
|
||||
|
||||
@@ -76,6 +76,10 @@ class SupplyRouteJs(BaseModel):
|
||||
def for_link(
|
||||
game: Game, a: ControlPoint, b: ControlPoint, points: list[Point], sea: bool
|
||||
) -> SupplyRouteJs:
|
||||
if a.captured.is_blue:
|
||||
blue = True
|
||||
else:
|
||||
blue = False
|
||||
return SupplyRouteJs(
|
||||
# Although these are not persistent objects in the backend, the frontend
|
||||
# needs unique IDs for anything that it will use in a list. That means that
|
||||
@@ -93,7 +97,7 @@ class SupplyRouteJs(BaseModel):
|
||||
points=[p.latlng() for p in points],
|
||||
front_active=not sea and a.front_is_active(b),
|
||||
is_sea=sea,
|
||||
blue=a.captured,
|
||||
blue=blue,
|
||||
active_transports=TransportFinder(game, a, b).describe_active_transports(
|
||||
sea
|
||||
),
|
||||
|
||||
@@ -34,12 +34,16 @@ class TgoJs(BaseModel):
|
||||
def for_tgo(tgo: TheaterGroundObject) -> TgoJs:
|
||||
threat_ranges = [group.max_threat_range().meters for group in tgo.groups]
|
||||
detection_ranges = [group.max_detection_range().meters for group in tgo.groups]
|
||||
if tgo.control_point.captured.is_blue:
|
||||
blue = True
|
||||
else:
|
||||
blue = False
|
||||
return TgoJs(
|
||||
id=tgo.id,
|
||||
name=tgo.name,
|
||||
control_point_name=tgo.control_point.name,
|
||||
category=tgo.category,
|
||||
blue=tgo.control_point.captured,
|
||||
blue=blue,
|
||||
position=tgo.position.latlng(),
|
||||
units=[unit.display_name for unit in tgo.units],
|
||||
threat_ranges=threat_ranges,
|
||||
|
||||
@@ -816,6 +816,12 @@ class Settings:
|
||||
GAMEPLAY_SECTION,
|
||||
default=True,
|
||||
)
|
||||
eplrs_enabled: bool = boolean_option(
|
||||
"Enable EPLRS",
|
||||
MISSION_GENERATOR_PAGE,
|
||||
GAMEPLAY_SECTION,
|
||||
default=True,
|
||||
)
|
||||
generate_dark_kneeboard: bool = boolean_option(
|
||||
"Generate dark kneeboard",
|
||||
MISSION_GENERATOR_PAGE,
|
||||
|
||||
@@ -46,7 +46,7 @@ class AirCombat(JoinableCombat):
|
||||
blue_flights = []
|
||||
red_flights = []
|
||||
for flight in self.flights:
|
||||
if flight.squadron.player:
|
||||
if flight.squadron.player.is_blue:
|
||||
blue_flights.append(str(flight))
|
||||
else:
|
||||
red_flights.append(str(flight))
|
||||
@@ -71,7 +71,7 @@ class AirCombat(JoinableCombat):
|
||||
blue = []
|
||||
red = []
|
||||
for flight in self.flights:
|
||||
if flight.squadron.player:
|
||||
if flight.squadron.player.is_blue:
|
||||
blue.append(flight)
|
||||
else:
|
||||
red.append(flight)
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections.abc import Iterator
|
||||
from datetime import timedelta
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.theater.player import Player
|
||||
from .aircombat import AirCombat
|
||||
from .aircraftengagementzones import AircraftEngagementZones
|
||||
from .atip import AtIp
|
||||
@@ -31,8 +32,10 @@ class CombatInitiator:
|
||||
def update_active_combats(self) -> None:
|
||||
blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato)
|
||||
red_a2a = AircraftEngagementZones.from_ato(self.game.red.ato)
|
||||
blue_sam = SamEngagementZones.from_theater(self.game.theater, player=True)
|
||||
red_sam = SamEngagementZones.from_theater(self.game.theater, player=False)
|
||||
blue_sam = SamEngagementZones.from_theater(
|
||||
self.game.theater, player=Player.BLUE
|
||||
)
|
||||
red_sam = SamEngagementZones.from_theater(self.game.theater, player=Player.RED)
|
||||
|
||||
# Check each vulnerable flight to see if it has initiated combat. If any flight
|
||||
# initiates combat, a single FrozenCombat will be created for all involved
|
||||
@@ -46,7 +49,7 @@ class CombatInitiator:
|
||||
if flight.state.in_combat:
|
||||
return
|
||||
|
||||
if flight.squadron.player:
|
||||
if flight.squadron.player.is_blue:
|
||||
a2a = red_a2a
|
||||
own_a2a = blue_a2a
|
||||
sam = red_sam
|
||||
|
||||
@@ -9,7 +9,7 @@ from shapely.ops import unary_union
|
||||
from game.utils import dcs_to_shapely_point
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater, TheaterGroundObject
|
||||
from game.theater import ConflictTheater, TheaterGroundObject, Player
|
||||
from game.threatzones import ThreatPoly
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ class SamEngagementZones:
|
||||
yield tgo
|
||||
|
||||
@classmethod
|
||||
def from_theater(cls, theater: ConflictTheater, player: bool) -> SamEngagementZones:
|
||||
def from_theater(
|
||||
cls, theater: ConflictTheater, player: Player
|
||||
) -> SamEngagementZones:
|
||||
commit_regions = []
|
||||
individual_zones = []
|
||||
for cp in theater.control_points_for(player):
|
||||
|
||||
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||
from game.ato import Flight, Package
|
||||
from game.navmesh import NavMesh
|
||||
from game.sim.combat import FrozenCombat
|
||||
from game.theater import ControlPoint, FrontLine, TheaterGroundObject
|
||||
from game.theater import ControlPoint, FrontLine, TheaterGroundObject, Player
|
||||
from game.threatzones import ThreatZones
|
||||
from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode
|
||||
|
||||
@@ -24,9 +24,9 @@ class GameUpdateEvents:
|
||||
updated_combats: list[FrozenCombat] = field(default_factory=list)
|
||||
ended_combats: list[FrozenCombat] = field(default_factory=list)
|
||||
updated_flight_positions: list[tuple[Flight, Point]] = field(default_factory=list)
|
||||
navmesh_updates: dict[bool, NavMesh] = field(default_factory=dict)
|
||||
navmesh_updates: dict[Player, NavMesh] = field(default_factory=dict)
|
||||
unculled_zones_updated: list[Point] = field(default_factory=list)
|
||||
threat_zones_updated: dict[bool, ThreatZones] = field(default_factory=dict)
|
||||
threat_zones_updated: dict[Player, ThreatZones] = field(default_factory=dict)
|
||||
new_flights: set[Flight] = field(default_factory=set)
|
||||
updated_flights: set[Flight] = field(default_factory=set)
|
||||
deleted_flights: set[UUID] = field(default_factory=set)
|
||||
@@ -70,7 +70,7 @@ class GameUpdateEvents:
|
||||
self.updated_flight_positions.append((flight, new_position))
|
||||
return self
|
||||
|
||||
def update_navmesh(self, player: bool, navmesh: NavMesh) -> GameUpdateEvents:
|
||||
def update_navmesh(self, player: Player, navmesh: NavMesh) -> GameUpdateEvents:
|
||||
self.navmesh_updates[player] = navmesh
|
||||
return self
|
||||
|
||||
@@ -78,7 +78,9 @@ class GameUpdateEvents:
|
||||
self.unculled_zones_updated = zones
|
||||
return self
|
||||
|
||||
def update_threat_zones(self, player: bool, zones: ThreatZones) -> GameUpdateEvents:
|
||||
def update_threat_zones(
|
||||
self, player: Player, zones: ThreatZones
|
||||
) -> GameUpdateEvents:
|
||||
self.threat_zones_updated[player] = zones
|
||||
return self
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ class MissionResultsProcessor:
|
||||
def commit_captures(self, debriefing: Debriefing, events: GameUpdateEvents) -> None:
|
||||
for captured in debriefing.base_captures:
|
||||
try:
|
||||
if captured.captured_by_player:
|
||||
if captured.captured_by_player.is_blue:
|
||||
self.game.message(
|
||||
f"{captured.control_point} captured!",
|
||||
f"We took control of {captured.control_point}.",
|
||||
@@ -174,7 +174,7 @@ class MissionResultsProcessor:
|
||||
self, debriefing: Debriefing, events: GameUpdateEvents
|
||||
) -> None:
|
||||
for cp in self.game.theater.player_points():
|
||||
enemy_cps = [e for e in cp.connected_points if not e.captured]
|
||||
enemy_cps = [e for e in cp.connected_points if e.captured.is_red]
|
||||
for enemy_cp in enemy_cps:
|
||||
front_line = cp.front_line_with(enemy_cp)
|
||||
front_line.update_position()
|
||||
@@ -321,7 +321,7 @@ class MissionResultsProcessor:
|
||||
settings = cp.coalition.game.settings
|
||||
factor = (
|
||||
settings.frontline_reserves_factor
|
||||
if cp.captured
|
||||
if cp.captured.is_blue
|
||||
else settings.frontline_reserves_factor_red
|
||||
)
|
||||
|
||||
@@ -345,7 +345,7 @@ class MissionResultsProcessor:
|
||||
1,
|
||||
(
|
||||
settings.reserves_procurement_target
|
||||
if source.captured
|
||||
if source.captured.is_blue
|
||||
else settings.reserves_procurement_target_red
|
||||
),
|
||||
)
|
||||
@@ -367,7 +367,7 @@ class MissionResultsProcessor:
|
||||
source.base.commit_losses(moved_units)
|
||||
|
||||
# Also transfer pending deliveries.
|
||||
for unit_type, count in source.ground_unit_orders.units.items():
|
||||
for unit_type, count in list(source.ground_unit_orders.units.items()):
|
||||
move_count = int(count * move_factor)
|
||||
source.ground_unit_orders.sell({unit_type: move_count})
|
||||
destination.ground_unit_orders.order({unit_type: move_count})
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from typing import Iterator, Optional, Sequence, TYPE_CHECKING
|
||||
from typing import Iterator, Optional, Sequence, TYPE_CHECKING, Any
|
||||
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
@@ -14,18 +14,31 @@ from ..utils import Distance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
from game.theater.player import Player
|
||||
from ..ato.flighttype import FlightType
|
||||
from .squadron import Squadron
|
||||
|
||||
|
||||
class AirWing:
|
||||
def __init__(self, player: bool, game: Game, faction: Faction) -> None:
|
||||
def __init__(self, player: Player, game: Game, faction: Faction) -> None:
|
||||
self.player = player
|
||||
self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
|
||||
self.squadron_defs = SquadronDefLoader(game, faction).load()
|
||||
self.squadron_def_generator = SquadronDefGenerator(faction)
|
||||
self.settings = game.settings
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Migration: Convert old boolean player values to Player enum
|
||||
if "player" in state and isinstance(state["player"], bool):
|
||||
from game.theater.player import Player
|
||||
|
||||
if state["player"]:
|
||||
state["player"] = Player.BLUE
|
||||
else:
|
||||
state["player"] = Player.RED
|
||||
|
||||
self.__dict__.update(state)
|
||||
|
||||
def unclaim_squadron_def(self, squadron: Squadron) -> None:
|
||||
if squadron.aircraft in self.squadron_defs:
|
||||
for squadron_def in self.squadron_defs[squadron.aircraft]:
|
||||
|
||||
@@ -15,6 +15,7 @@ from faker import Faker
|
||||
from game.ato import Flight, FlightType, Package
|
||||
from game.settings import AutoAtoBehavior, Settings
|
||||
from game.theater import ParkingType
|
||||
from game.theater.player import Player
|
||||
from .pilot import Pilot, PilotStatus
|
||||
from ..db.database import Database
|
||||
from ..radio.radios import RadioFrequency
|
||||
@@ -96,7 +97,7 @@ class Squadron:
|
||||
self._livery_pool: list[str] = []
|
||||
|
||||
@property
|
||||
def player(self) -> bool:
|
||||
def player(self) -> Player:
|
||||
return self.coalition.player
|
||||
|
||||
def assign_to_base(self, base: ControlPoint) -> None:
|
||||
@@ -134,7 +135,7 @@ class Squadron:
|
||||
return self.claim_new_pilot_if_allowed()
|
||||
|
||||
# For opfor, so player/AI option is irrelevant.
|
||||
if not self.player:
|
||||
if self.player != Player.BLUE:
|
||||
return self.available_pilots.pop()
|
||||
|
||||
preference = self.settings.auto_ato_behavior
|
||||
|
||||
@@ -2,5 +2,6 @@ from .base import *
|
||||
from .conflicttheater import *
|
||||
from .controlpoint import *
|
||||
from .frontline import FrontLine
|
||||
from .landmap import *
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import SamGroundObject
|
||||
|
||||
@@ -15,6 +15,7 @@ from .daytimemap import DaytimeMap
|
||||
from .frontline import FrontLine
|
||||
from .iadsnetwork.iadsnetwork import IadsNetwork
|
||||
from .landmap import poly_contains, load_landmap
|
||||
from .player import Player
|
||||
from .seasonalconditions import SeasonalConditions
|
||||
from ..utils import Heading
|
||||
|
||||
@@ -170,10 +171,10 @@ class ConflictTheater:
|
||||
return new_point
|
||||
|
||||
def control_points_for(
|
||||
self, player: bool, state_check: bool = False
|
||||
self, player: Player, state_check: bool = False
|
||||
) -> Iterator[ControlPoint]:
|
||||
for point in self.controlpoints:
|
||||
if point.captured == player:
|
||||
if point.captured is player:
|
||||
if not state_check:
|
||||
yield point
|
||||
elif point.is_carrier and point.runway_is_operational():
|
||||
@@ -182,14 +183,21 @@ class ConflictTheater:
|
||||
yield point
|
||||
|
||||
def player_points(self, state_check: bool = False) -> List[ControlPoint]:
|
||||
return list(self.control_points_for(player=True, state_check=state_check))
|
||||
return list(
|
||||
self.control_points_for(player=Player.BLUE, state_check=state_check)
|
||||
)
|
||||
|
||||
def conflicts(self) -> Iterator[FrontLine]:
|
||||
for cp in self.player_points():
|
||||
yield from cp.front_lines.values()
|
||||
|
||||
def enemy_points(self, state_check: bool = False) -> List[ControlPoint]:
|
||||
return list(self.control_points_for(player=False, state_check=state_check))
|
||||
return list(self.control_points_for(player=Player.RED, state_check=state_check))
|
||||
|
||||
def neutral_points(self, state_check: bool = False) -> List[ControlPoint]:
|
||||
return list(
|
||||
self.control_points_for(player=Player.NEUTRAL, state_check=state_check)
|
||||
)
|
||||
|
||||
def closest_control_point(
|
||||
self, point: Point, allow_naval: bool = False
|
||||
@@ -259,10 +267,12 @@ class ConflictTheater:
|
||||
"""
|
||||
closest_cps = list()
|
||||
distances_to_cp = dict()
|
||||
if cp.captured:
|
||||
if cp.captured.is_blue:
|
||||
control_points = self.player_points()
|
||||
else:
|
||||
elif cp.captured.is_red:
|
||||
control_points = self.enemy_points()
|
||||
elif cp.captured.is_neutral:
|
||||
control_points = self.neutral_points()
|
||||
for other_cp in control_points:
|
||||
if cp == other_cp:
|
||||
continue
|
||||
@@ -292,6 +302,12 @@ class ConflictTheater:
|
||||
return cp
|
||||
raise KeyError(f"Cannot find ControlPoint named {name}")
|
||||
|
||||
def control_point_by_full_name(self, full_name: str) -> ControlPoint:
|
||||
for cp in self.controlpoints:
|
||||
if cp.full_name == full_name:
|
||||
return cp
|
||||
raise KeyError(f"Cannot find ControlPoint with full name {full_name}")
|
||||
|
||||
def find_carriers(self) -> List[ControlPoint]:
|
||||
try:
|
||||
carriers = [cp for cp in self.controlpoints if cp.is_carrier]
|
||||
|
||||
@@ -40,12 +40,14 @@ from dcs.ships import (
|
||||
Stennis,
|
||||
Type_071,
|
||||
hms_invincible,
|
||||
Essex,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unitgroup import ShipGroup, StaticGroup
|
||||
from dcs.unittype import ShipType
|
||||
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.controlpoint_influenceradius import ControlPointInfluenceRadius
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.runways import RunwayAssigner, RunwayData
|
||||
@@ -66,6 +68,7 @@ from .base import Base
|
||||
from .frontline import FrontLine
|
||||
from .interfaces.CTLD import CTLD
|
||||
from .missiontarget import MissionTarget
|
||||
from .player import Player
|
||||
from .theatergroundobject import (
|
||||
GenericCarrierGroundObject,
|
||||
TheaterGroundObject,
|
||||
@@ -375,15 +378,17 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
position: Point,
|
||||
at: StartingPosition,
|
||||
theater: ConflictTheater,
|
||||
starts_blue: bool,
|
||||
starting_coalition: Player,
|
||||
cptype: ControlPointType = ControlPointType.AIRBASE,
|
||||
is_invisible: bool = False,
|
||||
) -> None:
|
||||
super().__init__(name, position)
|
||||
self.id = uuid.uuid4()
|
||||
self.full_name = name
|
||||
self.at = at
|
||||
self.theater = theater
|
||||
self.starts_blue = starts_blue
|
||||
self.is_invisible = is_invisible
|
||||
self.starting_coalition = starting_coalition
|
||||
self.connected_objectives: List[TheaterGroundObject] = []
|
||||
self.preset_locations = PresetLocations()
|
||||
self.helipads: List[PointWithHeading] = []
|
||||
@@ -414,6 +419,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
# Initialized late because ControlPoints are constructed before the game is.
|
||||
self._front_line_db: Database[FrontLine] | None = None
|
||||
|
||||
self.influence_radius: ControlPointInfluenceRadius | None = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__}: {self.name}>"
|
||||
|
||||
@@ -429,7 +436,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
|
||||
def finish_init(self, game: Game) -> None:
|
||||
assert self._coalition is None
|
||||
self._coalition = game.coalition_for(self.starts_blue)
|
||||
self._coalition = game.coalition_for(self.starting_coalition)
|
||||
assert self._front_line_db is None
|
||||
self._front_line_db = game.db.front_lines
|
||||
|
||||
@@ -438,7 +445,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
# the entire game state when it comes up.
|
||||
from game.sim import GameUpdateEvents
|
||||
|
||||
self._create_missing_front_lines(laser_code_registry, GameUpdateEvents())
|
||||
if self.captured != Player.NEUTRAL:
|
||||
self._create_missing_front_lines(laser_code_registry, GameUpdateEvents())
|
||||
|
||||
@property
|
||||
def front_line_db(self) -> Database[FrontLine]:
|
||||
@@ -449,9 +457,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
self, laser_code_registry: LaserCodeRegistry, events: GameUpdateEvents
|
||||
) -> None:
|
||||
for connection in self.convoy_routes.keys():
|
||||
if not connection.front_line_active_with(
|
||||
self
|
||||
) and not connection.is_friendly_to(self):
|
||||
if (
|
||||
not connection.front_line_active_with(self)
|
||||
and not connection.is_friendly_to(self)
|
||||
and connection.captured != Player.NEUTRAL
|
||||
):
|
||||
self._create_front_line_with(laser_code_registry, connection, events)
|
||||
|
||||
def _create_front_line_with(
|
||||
@@ -485,6 +495,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
|
||||
@property
|
||||
def has_frontline(self) -> bool:
|
||||
if self.captured.is_neutral:
|
||||
return False
|
||||
return bool(self.front_lines)
|
||||
|
||||
def front_line_active_with(self, other: ControlPoint) -> bool:
|
||||
@@ -494,14 +506,17 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
return self.front_lines[other]
|
||||
|
||||
@property
|
||||
def captured(self) -> bool:
|
||||
def captured(self) -> Player:
|
||||
return self.coalition.player
|
||||
|
||||
@property
|
||||
def standard_identity(self) -> StandardIdentity:
|
||||
return (
|
||||
StandardIdentity.FRIEND if self.captured else StandardIdentity.HOSTILE_FAKER
|
||||
)
|
||||
if self.captured.is_neutral:
|
||||
return StandardIdentity.UNKNOWN
|
||||
elif self.captured.is_blue:
|
||||
return StandardIdentity.FRIEND
|
||||
else:
|
||||
return StandardIdentity.HOSTILE_FAKER
|
||||
|
||||
@property
|
||||
def sidc_status(self) -> Status:
|
||||
@@ -813,7 +828,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
found.append(g)
|
||||
return found
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
def is_friendly(self, to_player: Player) -> bool:
|
||||
return self.captured == to_player
|
||||
|
||||
def is_friendly_to(self, control_point: ControlPoint) -> bool:
|
||||
@@ -822,7 +837,9 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
def capture_equipment(self, game: Game) -> None:
|
||||
total = self.base.total_armor_value
|
||||
self.base.armor.clear()
|
||||
game.adjust_budget(total, player=not self.captured)
|
||||
game.adjust_budget(
|
||||
total, player=Player.BLUE if self.captured.is_red else Player.RED
|
||||
)
|
||||
game.message(
|
||||
f"{self.name} is not connected to any friendly points. Ground "
|
||||
f"vehicles have been captured and sold for ${total}M."
|
||||
@@ -851,7 +868,9 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
|
||||
def capture_aircraft(self, game: Game, airframe: AircraftType, count: int) -> None:
|
||||
value = airframe.price * count
|
||||
game.adjust_budget(value, player=not self.captured)
|
||||
game.adjust_budget(
|
||||
value, player=Player.BLUE if self.captured.is_red else Player.RED
|
||||
)
|
||||
game.message(
|
||||
f"No valid retreat destination in range of {self.name} for {airframe} "
|
||||
f"{count} aircraft have been captured and sold for ${value}M."
|
||||
@@ -954,7 +973,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
pass
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
|
||||
def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
|
||||
new_coalition = game.coalition_for(for_player)
|
||||
self.ground_unit_orders.refund_all(self.coalition)
|
||||
self.retreat_ground_units(game)
|
||||
@@ -1119,6 +1138,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
|
||||
@property
|
||||
def has_active_frontline(self) -> bool:
|
||||
if self.captured.is_neutral:
|
||||
return False
|
||||
return any(not c.is_friendly(self.captured) for c in self.connected_points)
|
||||
|
||||
def front_is_active(self, other: ControlPoint) -> bool:
|
||||
@@ -1205,20 +1226,22 @@ class Airfield(ControlPoint, CTLD):
|
||||
self,
|
||||
airport: Airport,
|
||||
theater: ConflictTheater,
|
||||
starts_blue: bool,
|
||||
starting_coalition: Player,
|
||||
ctld_zones: Optional[List[Tuple[Point, float]]] = None,
|
||||
influence_zone: Optional[List[Tuple[Point, float]]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
airport.name,
|
||||
airport.position,
|
||||
airport,
|
||||
theater,
|
||||
starts_blue,
|
||||
starting_coalition,
|
||||
cptype=ControlPointType.AIRBASE,
|
||||
)
|
||||
self.airport = airport
|
||||
self._runway_status = RunwayStatus()
|
||||
self.ctld_zones = ctld_zones
|
||||
self.influence_zone = influence_zone
|
||||
|
||||
@property
|
||||
def dcs_airport(self) -> Airport:
|
||||
@@ -1244,7 +1267,7 @@ class Airfield(ControlPoint, CTLD):
|
||||
return True
|
||||
return self.runway_is_operational()
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
@@ -1365,7 +1388,7 @@ class NavalControlPoint(
|
||||
def is_fleet(self) -> bool:
|
||||
return True
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
@@ -1426,6 +1449,7 @@ class NavalControlPoint(
|
||||
CVN_72,
|
||||
CVN_73,
|
||||
CVN_75,
|
||||
Essex,
|
||||
]:
|
||||
return True
|
||||
return False
|
||||
@@ -1473,7 +1497,7 @@ class NavalControlPoint(
|
||||
|
||||
class Carrier(NavalControlPoint):
|
||||
def __init__(
|
||||
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
|
||||
self, name: str, at: Point, theater: ConflictTheater, starts_blue: Player
|
||||
):
|
||||
super().__init__(
|
||||
name,
|
||||
@@ -1488,7 +1512,7 @@ class Carrier(NavalControlPoint):
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER
|
||||
|
||||
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
|
||||
def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
|
||||
raise RuntimeError("Carriers cannot be captured")
|
||||
|
||||
@property
|
||||
@@ -1506,9 +1530,14 @@ class Carrier(NavalControlPoint):
|
||||
return "cv"
|
||||
|
||||
|
||||
class EssexCarrier(Carrier):
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
return aircraft.lha_capable
|
||||
|
||||
|
||||
class Lha(NavalControlPoint):
|
||||
def __init__(
|
||||
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
|
||||
self, name: str, at: Point, theater: ConflictTheater, starts_blue: Player
|
||||
):
|
||||
super().__init__(
|
||||
name, at, at, theater, starts_blue, cptype=ControlPointType.LHA_GROUP
|
||||
@@ -1518,7 +1547,7 @@ class Lha(NavalControlPoint):
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.AMPHIBIOUS_ASSAULT_SHIP_GENERAL
|
||||
|
||||
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
|
||||
def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
|
||||
raise RuntimeError("LHAs cannot be captured")
|
||||
|
||||
@property
|
||||
@@ -1541,7 +1570,7 @@ class OffMapSpawn(ControlPoint):
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
self, name: str, position: Point, theater: ConflictTheater, starts_blue: bool
|
||||
self, name: str, position: Point, theater: ConflictTheater, starts_blue: Player
|
||||
):
|
||||
super().__init__(
|
||||
name,
|
||||
@@ -1556,10 +1585,10 @@ class OffMapSpawn(ControlPoint):
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
return SymbolSet.LAND_INSTALLATIONS, LandInstallationEntity.AIPORT_AIR_BASE
|
||||
|
||||
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
|
||||
def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
|
||||
raise RuntimeError("Off map control points cannot be captured")
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
yield from []
|
||||
|
||||
def total_aircraft_parking(self, parking_type: ParkingType) -> int:
|
||||
@@ -1616,14 +1645,18 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
|
||||
name: str,
|
||||
at: Point,
|
||||
theater: ConflictTheater,
|
||||
starts_blue: bool,
|
||||
starts_blue: Player,
|
||||
ctld_zones: Optional[List[Tuple[Point, float]]] = None,
|
||||
is_invisible: bool = False,
|
||||
influence_zone: Optional[List[Tuple[Point, float]]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name, at, at, theater, starts_blue, cptype=ControlPointType.FOB
|
||||
)
|
||||
self.name = name
|
||||
self.ctld_zones = ctld_zones
|
||||
self.is_invisible = is_invisible
|
||||
self.influence_zone = influence_zone
|
||||
|
||||
@property
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
@@ -1649,7 +1682,7 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
return RunwayStatus()
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any, Iterator, List, TYPE_CHECKING, Tuple
|
||||
from dcs.mapping import Point
|
||||
|
||||
from .missiontarget import MissionTarget
|
||||
from .player import Player
|
||||
from ..lasercodes.lasercode import LaserCode
|
||||
from ..utils import Heading, pairwise
|
||||
|
||||
@@ -89,19 +90,22 @@ class FrontLine(MissionTarget):
|
||||
def update_position(self) -> None:
|
||||
self.position = self._compute_position()
|
||||
|
||||
def control_point_friendly_to(self, player: bool) -> ControlPoint:
|
||||
if player:
|
||||
def control_point_friendly_to(self, player: Player) -> ControlPoint:
|
||||
if player.is_blue:
|
||||
return self.blue_cp
|
||||
return self.red_cp
|
||||
|
||||
def control_point_hostile_to(self, player: bool) -> ControlPoint:
|
||||
return self.control_point_friendly_to(not player)
|
||||
def control_point_hostile_to(self, player: Player) -> ControlPoint:
|
||||
if player.is_blue:
|
||||
return self.red_cp
|
||||
else:
|
||||
return self.blue_cp
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
def is_friendly(self, to_player: Player) -> bool:
|
||||
"""Returns True if the objective is in friendly territory."""
|
||||
return False
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
yield from [
|
||||
@@ -210,6 +214,6 @@ class FrontLine(MissionTarget):
|
||||
raise ValueError(
|
||||
"Cannot sort control points that are friendly to each other"
|
||||
)
|
||||
if a.captured:
|
||||
if a.captured.is_blue:
|
||||
return a, b
|
||||
return b, a
|
||||
|
||||
@@ -16,6 +16,7 @@ from game.theater.theatergroundobject import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.theater.theatergroup import IadsGroundGroup
|
||||
from game.theater.player import Player
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
@@ -31,7 +32,7 @@ class SkynetNode:
|
||||
"""Dataclass for a SkynetNode used in the LUA Data table by the luagenerator"""
|
||||
|
||||
dcs_name: str
|
||||
player: bool
|
||||
player: Player
|
||||
iads_role: IadsRole
|
||||
properties: dict[str, str] = field(default_factory=dict)
|
||||
connections: dict[str, list[str]] = field(default_factory=lambda: defaultdict(list))
|
||||
@@ -62,7 +63,7 @@ class SkynetNode:
|
||||
def from_group(cls, group: IadsGroundGroup) -> SkynetNode:
|
||||
node = cls(
|
||||
cls.dcs_name_for_group(group),
|
||||
group.ground_object.is_friendly(True),
|
||||
group.ground_object.coalition.player,
|
||||
group.iads_role,
|
||||
)
|
||||
unit_type = group.units[0].unit_type
|
||||
@@ -314,8 +315,8 @@ class IadsNetwork:
|
||||
self._make_advanced_connections_by_range(node)
|
||||
|
||||
def _is_friendly(self, node: IadsNetworkNode, tgo: TheaterGroundObject) -> bool:
|
||||
node_friendly = node.group.ground_object.is_friendly(True)
|
||||
tgo_friendly = tgo.is_friendly(True)
|
||||
node_friendly = node.group.ground_object.is_friendly(Player.BLUE)
|
||||
tgo_friendly = tgo.is_friendly(Player.BLUE)
|
||||
return node_friendly == tgo_friendly
|
||||
|
||||
def _update_network(
|
||||
|
||||
@@ -12,6 +12,7 @@ from dcs.mission import Mission
|
||||
from dcs.terrain.terrain import Terrain
|
||||
from shapely import geometry, LineString
|
||||
from shapely.geometry import MultiPolygon, Polygon
|
||||
import shapely as shp
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -28,6 +29,11 @@ class Landmap:
|
||||
if not self.sea_zones.is_valid:
|
||||
raise RuntimeError("Sea zones not valid")
|
||||
|
||||
# Generate Spatial Index using `prepare` to improve performance
|
||||
shp.prepare(self.inclusion_zones)
|
||||
shp.prepare(self.exclusion_zones)
|
||||
shp.prepare(self.sea_zones)
|
||||
|
||||
@cached_property
|
||||
def inclusion_zone_only(self) -> MultiPolygon:
|
||||
return self.inclusion_zones - self.exclusion_zones - self.sea_zones
|
||||
|
||||
@@ -6,7 +6,7 @@ from dcs.mapping import Point
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.theater import TheaterUnit, Coalition
|
||||
from game.theater import TheaterUnit, Coalition, Player
|
||||
|
||||
|
||||
class MissionTarget:
|
||||
@@ -24,11 +24,11 @@ class MissionTarget:
|
||||
"""Computes the distance to the given mission target."""
|
||||
return self.position.distance_to_point(other.position)
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
def is_friendly(self, to_player: Player) -> bool:
|
||||
"""Returns True if the objective is in friendly territory."""
|
||||
raise NotImplementedError
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
|
||||
30
game/theater/player.py
Normal file
30
game/theater/player.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Player(Enum):
|
||||
NEUTRAL = "Neutral"
|
||||
BLUE = "Blue"
|
||||
RED = "Red"
|
||||
|
||||
@property
|
||||
def is_red(self) -> bool:
|
||||
"""Returns True if the player is Red."""
|
||||
return self == Player.RED
|
||||
|
||||
@property
|
||||
def is_blue(self) -> bool:
|
||||
"""Returns True if the player is Blue."""
|
||||
return self == Player.BLUE
|
||||
|
||||
@property
|
||||
def is_neutral(self) -> bool:
|
||||
"""Returns True if the player is Neutral."""
|
||||
return self == Player.NEUTRAL
|
||||
|
||||
@property
|
||||
def opponent(self) -> "Player":
|
||||
"""Returns the opponent player."""
|
||||
if self.is_blue:
|
||||
return Player.RED
|
||||
else:
|
||||
return Player.BLUE
|
||||
@@ -13,7 +13,12 @@ from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.naming import namegen
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.theater import PointWithHeading, PresetLocation, NavalControlPoint
|
||||
from game.theater import (
|
||||
PointWithHeading,
|
||||
PresetLocation,
|
||||
NavalControlPoint,
|
||||
EssexCarrier,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
IadsBuildingGroundObject,
|
||||
@@ -27,6 +32,7 @@ from . import (
|
||||
Fob,
|
||||
OffMapSpawn,
|
||||
)
|
||||
from .player import Player
|
||||
from .theatergroup import (
|
||||
IadsGroundGroup,
|
||||
IadsRole,
|
||||
@@ -90,11 +96,13 @@ class ModSettings:
|
||||
uh_60l: bool = False
|
||||
jas39_gripen: bool = False
|
||||
sk_60: bool = False
|
||||
mam: bool = False
|
||||
mirage_3: bool = False
|
||||
super_etendard: bool = False
|
||||
su15_flagon: bool = False
|
||||
su30_flanker_h: bool = False
|
||||
su57_felon: bool = False
|
||||
tornado_adv: bool = False
|
||||
frenchpack: bool = False
|
||||
high_digit_sams: bool = False
|
||||
ov10a_bronco: bool = False
|
||||
@@ -151,12 +159,12 @@ class GameGenerator:
|
||||
game.settings.version = VERSION
|
||||
return game
|
||||
|
||||
def should_remove_carrier(self, player: bool) -> bool:
|
||||
faction = self.player if player else self.enemy
|
||||
def should_remove_carrier(self, player: Player) -> bool:
|
||||
faction = self.player if player.is_blue else self.enemy
|
||||
return self.generator_settings.no_carrier or not faction.carriers
|
||||
|
||||
def should_remove_lha(self, player: bool) -> bool:
|
||||
faction = self.player if player else self.enemy
|
||||
def should_remove_lha(self, player: Player) -> bool:
|
||||
faction = self.player if player.is_blue else self.enemy
|
||||
return self.generator_settings.no_lha or not [
|
||||
x for x in faction.carriers if x.unit_class == UnitClass.HELICOPTER_CARRIER
|
||||
]
|
||||
@@ -167,11 +175,14 @@ class GameGenerator:
|
||||
# Remove carrier and lha, invert situation if needed
|
||||
for cp in self.theater.controlpoints:
|
||||
if self.generator_settings.inverted:
|
||||
cp.starts_blue = cp.captured_invert
|
||||
if not cp.starting_coalition.is_neutral:
|
||||
cp.starting_coalition = (
|
||||
Player.RED if not cp.captured_invert else Player.BLUE
|
||||
)
|
||||
|
||||
if cp.is_carrier and self.should_remove_carrier(cp.starts_blue):
|
||||
if cp.is_carrier and self.should_remove_carrier(cp.starting_coalition):
|
||||
to_remove.append(cp)
|
||||
elif cp.is_lha and self.should_remove_lha(cp.starts_blue):
|
||||
elif cp.is_lha and self.should_remove_lha(cp.starting_coalition):
|
||||
to_remove.append(cp)
|
||||
|
||||
# do remove
|
||||
@@ -223,11 +234,13 @@ class ControlPointGroundObjectGenerator:
|
||||
self.control_point.connected_objectives.append(ground_object)
|
||||
|
||||
def generate_navy(self) -> None:
|
||||
if self.control_point.captured.is_neutral:
|
||||
return
|
||||
skip_player_navy = self.generator_settings.no_player_navy
|
||||
if self.control_point.captured and skip_player_navy:
|
||||
if self.control_point.captured.is_blue and skip_player_navy:
|
||||
return
|
||||
skip_enemy_navy = self.generator_settings.no_enemy_navy
|
||||
if not self.control_point.captured and skip_enemy_navy:
|
||||
if self.control_point.captured.is_red and skip_enemy_navy:
|
||||
return
|
||||
for position in self.control_point.preset_locations.ships:
|
||||
unit_group = self.armed_forces.random_group_for_task(GroupTask.NAVY)
|
||||
@@ -296,7 +309,9 @@ class GenericCarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
if go.category in ["CARRIER", "LHA"]
|
||||
][0]
|
||||
groups = [
|
||||
g for g in carrier_go.groups if "Carrier" in g.name or "LHA" in g.name
|
||||
g
|
||||
for g in carrier_go.groups
|
||||
if "carrier" in g.name.lower() or "lha" in g.name.lower()
|
||||
]
|
||||
return groups[0].units[0]
|
||||
|
||||
@@ -319,6 +334,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
|
||||
logging.error(f"{self.faction_name} has no access to AircraftCarrier")
|
||||
return False
|
||||
|
||||
self.transform_to_essex_if_needed(unit_group)
|
||||
self.generate_ground_object_from_group(
|
||||
unit_group,
|
||||
PresetLocation(
|
||||
@@ -331,6 +347,25 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
|
||||
self.apply_carrier_config()
|
||||
return True
|
||||
|
||||
def transform_to_essex_if_needed(self, unit_group: ForceGroup) -> None:
|
||||
classes = [u.unit_class for u in unit_group.units]
|
||||
if any([c for c in classes if c == UnitClass.HELICOPTER_CARRIER]) and not any(
|
||||
[c for c in classes if c == UnitClass.AIRCRAFT_CARRIER]
|
||||
):
|
||||
self.game.theater.controlpoints.remove(self.control_point)
|
||||
sqdrns = self.control_point.squadrons
|
||||
self.control_point = EssexCarrier(
|
||||
self.control_point.name,
|
||||
self.control_point.position,
|
||||
self.game.theater,
|
||||
self.control_point.starting_coalition,
|
||||
)
|
||||
self.control_point.finish_init(self.game)
|
||||
self.game.theater.controlpoints.append(self.control_point)
|
||||
for sqdrn in sqdrns:
|
||||
if sqdrn.aircraft.lha_capable:
|
||||
sqdrn.location = self.control_point
|
||||
|
||||
|
||||
class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
@@ -586,14 +621,24 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
return False
|
||||
|
||||
def generate_fob(self) -> None:
|
||||
self.generate_building_at(
|
||||
GroupTask.FOB,
|
||||
PresetLocation(
|
||||
self.control_point.name,
|
||||
self.control_point.position,
|
||||
self.control_point.heading,
|
||||
),
|
||||
)
|
||||
if self.control_point.is_invisible:
|
||||
self.generate_building_at(
|
||||
GroupTask.INVISIBLE_FOB,
|
||||
PresetLocation(
|
||||
self.control_point.name,
|
||||
self.control_point.position,
|
||||
self.control_point.heading,
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.generate_building_at(
|
||||
GroupTask.FOB,
|
||||
PresetLocation(
|
||||
self.control_point.name,
|
||||
self.control_point.position,
|
||||
self.control_point.heading,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class GroundObjectGenerator:
|
||||
|
||||
@@ -21,6 +21,7 @@ from game.sidc import (
|
||||
)
|
||||
from game.theater.presetlocation import PresetLocation
|
||||
from .missiontarget import MissionTarget
|
||||
from .player import Player
|
||||
from ..data.groups import GroupTask
|
||||
from ..utils import Distance, Heading, meters
|
||||
|
||||
@@ -89,6 +90,8 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
|
||||
|
||||
@property
|
||||
def sidc_status(self) -> Status:
|
||||
if self.control_point.captured.is_neutral:
|
||||
return Status.PRESENT
|
||||
if self.is_dead:
|
||||
return Status.PRESENT_DESTROYED
|
||||
elif self.dead_units:
|
||||
@@ -98,11 +101,12 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
|
||||
|
||||
@property
|
||||
def standard_identity(self) -> StandardIdentity:
|
||||
return (
|
||||
StandardIdentity.FRIEND
|
||||
if self.control_point.captured
|
||||
else StandardIdentity.HOSTILE_FAKER
|
||||
)
|
||||
if self.control_point.captured.is_blue:
|
||||
return StandardIdentity.FRIEND
|
||||
elif self.control_point.captured.is_neutral:
|
||||
return StandardIdentity.UNKNOWN
|
||||
else:
|
||||
return StandardIdentity.HOSTILE_FAKER
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
@@ -154,10 +158,12 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
|
||||
def faction_color(self) -> str:
|
||||
return "BLUE" if self.control_point.captured else "RED"
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
def is_friendly(self, to_player: Player) -> bool:
|
||||
if self.control_point.captured.is_neutral:
|
||||
return False
|
||||
return self.control_point.is_friendly(to_player)
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
@@ -360,7 +366,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
|
||||
|
||||
class NavalGroundObject(TheaterGroundObject, ABC):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
@@ -466,7 +472,7 @@ class MissileSiteGroundObject(TheaterGroundObject):
|
||||
def should_head_to_conflict(self) -> bool:
|
||||
return True
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
@@ -507,7 +513,7 @@ class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
def should_head_to_conflict(self) -> bool:
|
||||
return True
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
@@ -534,7 +540,7 @@ class IadsGroundObject(TheaterGroundObject, ABC):
|
||||
task=task,
|
||||
)
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
@@ -571,6 +577,8 @@ class SamGroundObject(IadsGroundObject):
|
||||
|
||||
@property
|
||||
def sidc_status(self) -> Status:
|
||||
if self.control_point.captured.is_neutral:
|
||||
return Status.PRESENT
|
||||
if self.is_dead:
|
||||
return Status.PRESENT_DESTROYED
|
||||
elif self.dead_units:
|
||||
@@ -585,7 +593,7 @@ class SamGroundObject(IadsGroundObject):
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
return SymbolSet.LAND_UNIT, LandUnitEntity.AIR_DEFENSE
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
@@ -642,7 +650,7 @@ class VehicleGroupGroundObject(TheaterGroundObject):
|
||||
def should_head_to_conflict(self) -> bool:
|
||||
return True
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
@@ -697,7 +705,7 @@ class ShipGroundObject(NavalGroundObject):
|
||||
|
||||
|
||||
class IadsBuildingGroundObject(BuildingGroundObject):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
|
||||
@@ -8,7 +8,7 @@ from dcs.triggers import TriggerZone
|
||||
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
|
||||
|
||||
from game.data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
|
||||
from game.data.units import ANTI_AIR_UNIT_CLASSES
|
||||
from game.data.units import ANTI_AIR_UNIT_CLASSES, UnitClass
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
@@ -82,9 +82,17 @@ class TheaterUnit:
|
||||
iads = self.ground_object.control_point.coalition.game.theater.iads_network
|
||||
iads.update_tgo(self.ground_object, events)
|
||||
if self.ground_object.is_naval_control_point:
|
||||
cp = self.ground_object.control_point
|
||||
for squadron in cp.squadrons:
|
||||
cp.coalition.air_wing.squadrons[squadron.aircraft].remove(squadron)
|
||||
for unit in self.ground_object.units:
|
||||
if (
|
||||
unit.unit_type
|
||||
and unit.unit_type.unit_class is UnitClass.AIRCRAFT_CARRIER
|
||||
and not unit.alive
|
||||
):
|
||||
cp = self.ground_object.control_point
|
||||
for squadron in cp.squadrons:
|
||||
cp.coalition.air_wing.squadrons[squadron.aircraft].remove(
|
||||
squadron
|
||||
)
|
||||
|
||||
def revive(self, events: GameUpdateEvents) -> None:
|
||||
self.alive = True
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user