mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Introduce support for "rebel-zones"
This commit is contained in:
parent
942faeeaa3
commit
431492fa77
@ -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
|
* **[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
|
* **[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
|
* **[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
|
## Fixes
|
||||||
* **[UI/UX]** A-10A flights can be edited again
|
* **[UI/UX]** A-10A flights can be edited again
|
||||||
|
|||||||
@ -595,9 +595,18 @@ class MizCampaignLoader:
|
|||||||
self.add_preset_locations()
|
self.add_preset_locations()
|
||||||
self.add_supply_routes()
|
self.add_supply_routes()
|
||||||
self.add_shipping_lanes()
|
self.add_shipping_lanes()
|
||||||
|
self.add_rebel_zones()
|
||||||
|
|
||||||
def get_ctld_zones(self, prefix: str) -> List[Tuple[Point, float]]:
|
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]
|
zones = [t for t in self.mission.triggers.zones() if prefix + " CTLD" in t.name]
|
||||||
for z in zones:
|
for z in zones:
|
||||||
self.mission.triggers.zones().remove(z)
|
self.mission.triggers.zones().remove(z)
|
||||||
return [(z.position, z.radius) for z in zones]
|
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)
|
||||||
|
|||||||
@ -42,6 +42,7 @@ class Migrator:
|
|||||||
self._update_weather()
|
self._update_weather()
|
||||||
self._update_tgos()
|
self._update_tgos()
|
||||||
self._reload_terrain()
|
self._reload_terrain()
|
||||||
|
self._update_theather()
|
||||||
|
|
||||||
# TODO: remove in due time as this is supposedly fixed
|
# TODO: remove in due time as this is supposedly fixed
|
||||||
self.game.settings.nevatim_parking_fix = False
|
self.game.settings.nevatim_parking_fix = False
|
||||||
@ -257,3 +258,7 @@ class Migrator:
|
|||||||
t = self.game.theater.terrain
|
t = self.game.theater.terrain
|
||||||
if issubclass(t.__class__, Terrain):
|
if issubclass(t.__class__, Terrain):
|
||||||
self.game.theater.terrain = type(t)() # type: ignore
|
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 = []
|
||||||
|
|||||||
@ -36,6 +36,7 @@ from .frontlineconflictdescription import FrontLineConflictDescription
|
|||||||
from .kneeboard import KneeboardGenerator
|
from .kneeboard import KneeboardGenerator
|
||||||
from .luagenerator import LuaGenerator
|
from .luagenerator import LuaGenerator
|
||||||
from .missiondata import MissionData
|
from .missiondata import MissionData
|
||||||
|
from .rebelliongenerator import RebellionGenerator
|
||||||
from .tgogenerator import TgoGenerator
|
from .tgogenerator import TgoGenerator
|
||||||
from .triggergenerator import TriggerGenerator
|
from .triggergenerator import TriggerGenerator
|
||||||
from .visualsgenerator import VisualsGenerator
|
from .visualsgenerator import VisualsGenerator
|
||||||
@ -111,6 +112,7 @@ class MissionGenerator:
|
|||||||
self.generate_ground_conflicts()
|
self.generate_ground_conflicts()
|
||||||
self.generate_air_units(tgo_generator)
|
self.generate_air_units(tgo_generator)
|
||||||
|
|
||||||
|
RebellionGenerator(self.mission, self.game).generate()
|
||||||
TriggerGenerator(self.mission, self.game).generate()
|
TriggerGenerator(self.mission, self.game).generate()
|
||||||
ForcedOptionsGenerator(self.mission, self.game).generate()
|
ForcedOptionsGenerator(self.mission, self.game).generate()
|
||||||
VisualsGenerator(self.mission, self.game).generate()
|
VisualsGenerator(self.mission, self.game).generate()
|
||||||
|
|||||||
101
game/missiongenerator/rebelliongenerator.py
Normal file
101
game/missiongenerator/rebelliongenerator.py
Normal 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
|
||||||
@ -8,6 +8,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.terrain.terrain import Terrain
|
from dcs.terrain.terrain import Terrain
|
||||||
|
from dcs.triggers import TriggerZone
|
||||||
from shapely import geometry, ops
|
from shapely import geometry, ops
|
||||||
|
|
||||||
from .daytimemap import DaytimeMap
|
from .daytimemap import DaytimeMap
|
||||||
@ -43,6 +44,7 @@ class ConflictTheater:
|
|||||||
self.seasonal_conditions = seasonal_conditions
|
self.seasonal_conditions = seasonal_conditions
|
||||||
self.daytime_map = daytime_map
|
self.daytime_map = daytime_map
|
||||||
self.controlpoints: list[ControlPoint] = []
|
self.controlpoints: list[ControlPoint] = []
|
||||||
|
self.rebel_zones: list[TriggerZone] = []
|
||||||
|
|
||||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
if "landmap_path" not in state:
|
if "landmap_path" not in state:
|
||||||
@ -66,6 +68,25 @@ class ConflictTheater:
|
|||||||
return theater_dir / "landmap.p"
|
return theater_dir / "landmap.p"
|
||||||
raise RuntimeError(f"Could not determine landmap path for {terrain_name}")
|
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:
|
def add_controlpoint(self, point: ControlPoint) -> None:
|
||||||
self.controlpoints.append(point)
|
self.controlpoints.append(point)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user