Introduce support for "rebel-zones"

This commit is contained in:
Raffson 2025-01-19 19:06:19 +01:00
parent 942faeeaa3
commit 431492fa77
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
6 changed files with 139 additions and 0 deletions

View File

@ -46,6 +46,7 @@
* **[UX]** Reduce size of save-file by loading landmap data on the fly, which also implies no new campaign needs to be started to benefit from an updated landmap
* **[New Game Wizard]** Ability to save an edited faction during new game creation
* **[Options]** New option to make AI helicopters prefer vertical takeoff and landing
* **[Campaign Design/Mission Generation]** Introduction of "rebel zones" which randomly spawn units according to the campaign's definitions.
## Fixes
* **[UI/UX]** A-10A flights can be edited again

View File

@ -595,9 +595,18 @@ class MizCampaignLoader:
self.add_preset_locations()
self.add_supply_routes()
self.add_shipping_lanes()
self.add_rebel_zones()
def get_ctld_zones(self, prefix: str) -> List[Tuple[Point, float]]:
zones = [t for t in self.mission.triggers.zones() if prefix + " CTLD" in t.name]
for z in zones:
self.mission.triggers.zones().remove(z)
return [(z.position, z.radius) for z in zones]
def add_rebel_zones(self) -> None:
zones = [
t for t in self.mission.triggers.zones() if t.name.startswith("Rebels")
]
for z in zones:
self.mission.triggers.zones().remove(z)
self.theater.add_rebel_zones(zones)

View File

@ -42,6 +42,7 @@ class Migrator:
self._update_weather()
self._update_tgos()
self._reload_terrain()
self._update_theather()
# TODO: remove in due time as this is supposedly fixed
self.game.settings.nevatim_parking_fix = False
@ -257,3 +258,7 @@ class Migrator:
t = self.game.theater.terrain
if issubclass(t.__class__, Terrain):
self.game.theater.terrain = type(t)() # type: ignore
def _update_theather(self) -> None:
if not hasattr(self.game.theater, "rebel_zones"):
self.game.theater.rebel_zones = []

View File

@ -36,6 +36,7 @@ from .frontlineconflictdescription import FrontLineConflictDescription
from .kneeboard import KneeboardGenerator
from .luagenerator import LuaGenerator
from .missiondata import MissionData
from .rebelliongenerator import RebellionGenerator
from .tgogenerator import TgoGenerator
from .triggergenerator import TriggerGenerator
from .visualsgenerator import VisualsGenerator
@ -111,6 +112,7 @@ class MissionGenerator:
self.generate_ground_conflicts()
self.generate_air_units(tgo_generator)
RebellionGenerator(self.mission, self.game).generate()
TriggerGenerator(self.mission, self.game).generate()
ForcedOptionsGenerator(self.mission, self.game).generate()
VisualsGenerator(self.mission, self.game).generate()

View File

@ -0,0 +1,101 @@
from __future__ import annotations
import logging
import random
from typing import TYPE_CHECKING
import numpy as np
import shapely.geometry
from dcs import Mission, Point
from dcs.country import Country
from dcs.triggers import TriggerZone, TriggerZoneCircular, TriggerZoneQuadPoint
from dcs.vehicles import vehicle_map
from game.dcs.groundunittype import GroundUnitType
from game.naming import namegen
if TYPE_CHECKING:
from game import Game
class RebellionGenerator:
def __init__(self, mission: Mission, game: Game) -> None:
self.mission = mission
self.game = game
def generate(self) -> None:
ownfor_country = self.mission.country(
self.game.coalition_for(player=True).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
)
for rz in self.game.theater.opfor_rebel_zones:
self._generate_rebel_zone(opfor_country, rz)
def _generate_rebel_zone(self, ownfor_country: Country, rz: TriggerZone) -> None:
for i, key_value_dict in rz.properties.items():
unit_id = key_value_dict["key"]
count_range = key_value_dict["value"]
if unit_id not in vehicle_map:
logging.warning(
f"Invalid unit_id '{unit_id}' in rebel zone '{rz.name}'"
)
continue
count, success = self._get_random_count_for_type(count_range)
if not success:
logging.warning(
f"Invalid count/range ({count_range}) for '{unit_id}' in rebel-zone '{rz.name}'"
)
continue
unit_type = vehicle_map[unit_id]
for _ in range(count):
location = self.get_random_point_in_zone(rz)
group = self.mission.vehicle_group(
ownfor_country,
namegen.next_unit_name(
ownfor_country, next(GroundUnitType.for_dcs_type(unit_type))
),
unit_type,
location,
heading=random.random() * 360,
)
group.hidden_on_mfd = True
group.hidden_on_planner = True
def get_random_point_in_zone(self, zone: TriggerZone) -> Point:
if isinstance(zone, TriggerZoneCircular):
shape = shapely.geometry.Point(zone.position.x, zone.position.y).buffer(
zone.radius
)
elif isinstance(zone, TriggerZoneQuadPoint):
shape = shapely.geometry.Polygon([[p.x, p.y] for p in zone.verticies])
else:
raise RuntimeError("Incompatible trigger-zone")
minx, miny, maxx, maxy = shape.bounds
p = self._random_shapely_point(maxx, maxy, minx, miny)
while not shape.contains(p):
p = self._random_shapely_point(maxx, maxy, minx, miny)
return zone.position.new_in_same_map(p.x, p.y)
@staticmethod
def _random_shapely_point(
maxx: float, maxy: float, minx: float, miny: float
) -> shapely.geometry.Point:
x = np.random.uniform(minx, maxx)
y = np.random.uniform(miny, maxy)
p = shapely.geometry.Point(x, y)
return p
@staticmethod
def _get_random_count_for_type(bounds: str) -> tuple[int, bool]:
parts = bounds.split("-")
if len(parts) == 1 and parts[0].isdigit():
return int(parts[0]), True
elif len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
return random.randint(int(parts[0]), int(parts[1])), True
else:
return 0, False

View File

@ -8,6 +8,7 @@ from uuid import UUID
from dcs.mapping import Point
from dcs.terrain.terrain import Terrain
from dcs.triggers import TriggerZone
from shapely import geometry, ops
from .daytimemap import DaytimeMap
@ -43,6 +44,7 @@ class ConflictTheater:
self.seasonal_conditions = seasonal_conditions
self.daytime_map = daytime_map
self.controlpoints: list[ControlPoint] = []
self.rebel_zones: list[TriggerZone] = []
def __setstate__(self, state: dict[str, Any]) -> None:
if "landmap_path" not in state:
@ -66,6 +68,25 @@ class ConflictTheater:
return theater_dir / "landmap.p"
raise RuntimeError(f"Could not determine landmap path for {terrain_name}")
def add_rebel_zones(self, zones: List[TriggerZone]) -> None:
self.rebel_zones.extend(zones)
@property
def opfor_rebel_zones(self) -> Iterator[TriggerZone]:
for rz in self.rebel_zones:
if {1: 1, 2: 0, 3: 0} == {
k: v for k, v in rz.color.items() if k in [1, 2, 3]
}:
yield rz
@property
def ownfor_rebel_zones(self) -> Iterator[TriggerZone]:
for rz in self.rebel_zones:
if {1: 0, 2: 0, 3: 1} == {
k: v for k, v in rz.color.items() if k in [1, 2, 3]
}:
yield rz
def add_controlpoint(self, point: ControlPoint) -> None:
self.controlpoints.append(point)