mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
* Don't generate runway data for heliports. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2710. * Remove dead code. * Implemented support for Chiller Juice Studios F/A-18E/F/G Super Hornet mod The Chiller Juice Studios Super Hornet mod works like this: it changes the stock F/A-18C Hornet into F/A-18E/F/G Super Hornet / Growler. The exact variant is selected with argument 999 in the livery description.lua, which is why I chose to add the three variants in the FA-18C_hornet.yaml. This way, we can use the squadrons mechanism in Liberation to select the appropriate variant by specifying the correct livery for the squadron. Current properties injected are wingspan / width and the custom ordnance added by the mod. Added F/A-18E/F/G banner by Schmokedpancake and F/A-18F icon. Resolves https://github.com/dcs-liberation/dcs_liberation/issues/2681 * Added a separate loadout file for the Chiller Juice Studios F/A-18E/F/G Super Hornet mod. Currently only replaces the FPU-8A fuel tanks with FPU-12s. * Added the possibility to use the AI variant of the F/A-18C in campaigns, allowing different loadouts and in the future, the Super Hornet mod alongside legacy Hornets in the same campaign. * Updated Chiller Juice Studios F/A-18E/F/G Super Hornet mod support to version 2.0. Removed the 1.x version property and pylon injection since they are no longer necessary, since 2.0 adds the Super Hornet variants as separate aircraft. For the same reason, removed the AI-only F/A-18C from the faction files (still retained the aircraft yaml, loadout files and icon/banner in case someone still wants to use it). Includes F/A-18E/F/G banner by Schmokedpancake, loadouts by @sgtfuzzle17 and F/A-18E/F icons. * Added Super Hornet, Growler squadrons and Growler banner by @sgtfuzzle17 The squadrons include the model of the airframe in their name, so they can be referenced directly from campaign yaml files without the risk of conflicting with the same squadron of a different era, flying a different airframe. Also updated the E and G model icons. Resolves #77 * Fixed a bug with the EA-18G banner not being visible in Retribution. Also added the Super Hornet variants to factions bluefor_modern and Israel-USN_2005_Allied_Sword. * Corrected the descriptions for tandem-seat Super Hornet variants. * Updated Chiller Juice Studios F/A-18E/F/G Super Hornet mod support to version 2.1 * Anti-ship loadouts are now named properly. * Update changelog.md * Update QNewGameWizard.py --------- Co-authored-by: Dan Albert <dan@gingerhq.net> Co-authored-by: Raffson <Raffson@users.noreply.github.com>
527 lines
19 KiB
Python
527 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import random
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, time
|
|
from typing import List, Optional
|
|
|
|
import dcs.statics
|
|
|
|
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
|
|
from game.theater.theatergroundobject import (
|
|
BuildingGroundObject,
|
|
IadsBuildingGroundObject,
|
|
)
|
|
from game.utils import Heading, escape_string_for_lua
|
|
from game.version import VERSION
|
|
from . import (
|
|
ConflictTheater,
|
|
ControlPoint,
|
|
ControlPointType,
|
|
Fob,
|
|
OffMapSpawn,
|
|
)
|
|
from .theatergroup import IadsGroundGroup, IadsRole, SceneryUnit, TheaterGroup
|
|
from ..armedforces.armedforces import ArmedForces
|
|
from ..armedforces.forcegroup import ForceGroup
|
|
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
|
from ..campaignloader.campaigngroundconfig import TgoConfig
|
|
from ..data.groups import GroupTask
|
|
from ..profiling import logged_duration
|
|
from ..settings import Settings
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GeneratorSettings:
|
|
start_date: datetime
|
|
start_time: time | None
|
|
player_budget: int
|
|
enemy_budget: int
|
|
inverted: bool
|
|
advanced_iads: bool
|
|
no_carrier: bool
|
|
no_lha: bool
|
|
no_player_navy: bool
|
|
no_enemy_navy: bool
|
|
tgo_config: TgoConfig
|
|
|
|
|
|
@dataclass
|
|
class ModSettings:
|
|
a4_skyhawk: bool = False
|
|
a6a_intruder: bool = False
|
|
f4bc_phantom: bool = False
|
|
f15d_baz: bool = False
|
|
f_16_idf: bool = False
|
|
fa_18efg: bool = False
|
|
f22_raptor: bool = False
|
|
f84g_thunderjet: bool = False
|
|
f100_supersabre: bool = False
|
|
f104_starfighter: bool = False
|
|
f105_thunderchief: bool = False
|
|
hercules: bool = False
|
|
uh_60l: bool = False
|
|
jas39_gripen: bool = False
|
|
su30_flanker_h: bool = False
|
|
su57_felon: bool = False
|
|
frenchpack: bool = False
|
|
high_digit_sams: bool = False
|
|
ov10a_bronco: bool = False
|
|
swedishmilitaryassetspack: bool = False
|
|
|
|
|
|
class GameGenerator:
|
|
def __init__(
|
|
self,
|
|
player: Faction,
|
|
enemy: Faction,
|
|
theater: ConflictTheater,
|
|
air_wing_config: CampaignAirWingConfig,
|
|
settings: Settings,
|
|
generator_settings: GeneratorSettings,
|
|
mod_settings: ModSettings,
|
|
) -> None:
|
|
self.player = player
|
|
self.enemy = enemy
|
|
self.theater = theater
|
|
self.air_wing_config = air_wing_config
|
|
self.settings = settings
|
|
self.generator_settings = generator_settings
|
|
self.player.apply_mod_settings(mod_settings)
|
|
self.enemy.apply_mod_settings(mod_settings)
|
|
|
|
def generate(self) -> Game:
|
|
with logged_duration("TGO population"):
|
|
# Reset name generator
|
|
namegen.reset()
|
|
self.prepare_theater()
|
|
game = Game(
|
|
player_faction=self.player,
|
|
enemy_faction=self.enemy,
|
|
theater=self.theater,
|
|
air_wing_config=self.air_wing_config,
|
|
start_date=self.generator_settings.start_date,
|
|
start_time=self.generator_settings.start_time,
|
|
settings=self.settings,
|
|
player_budget=self.generator_settings.player_budget,
|
|
enemy_budget=self.generator_settings.enemy_budget,
|
|
)
|
|
|
|
GroundObjectGenerator(game, self.generator_settings).generate()
|
|
game.settings.version = VERSION
|
|
return game
|
|
|
|
def should_remove_carrier(self, player: bool) -> bool:
|
|
faction = self.player if player else self.enemy
|
|
return self.generator_settings.no_carrier or not faction.carrier_names
|
|
|
|
def should_remove_lha(self, player: bool) -> bool:
|
|
faction = self.player if player else self.enemy
|
|
return self.generator_settings.no_lha or not faction.helicopter_carrier_names
|
|
|
|
def prepare_theater(self) -> None:
|
|
to_remove: List[ControlPoint] = []
|
|
|
|
# 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 cp.is_carrier and self.should_remove_carrier(cp.starts_blue):
|
|
to_remove.append(cp)
|
|
elif cp.is_lha and self.should_remove_lha(cp.starts_blue):
|
|
to_remove.append(cp)
|
|
|
|
# do remove
|
|
for cp in to_remove:
|
|
self.theater.controlpoints.remove(cp)
|
|
|
|
|
|
class ControlPointGroundObjectGenerator:
|
|
def __init__(
|
|
self,
|
|
game: Game,
|
|
generator_settings: GeneratorSettings,
|
|
control_point: ControlPoint,
|
|
) -> None:
|
|
self.game = game
|
|
self.generator_settings = generator_settings
|
|
self.control_point = control_point
|
|
|
|
@property
|
|
def faction_name(self) -> str:
|
|
return self.faction.name
|
|
|
|
@property
|
|
def faction(self) -> Faction:
|
|
return self.game.coalition_for(self.control_point.captured).faction
|
|
|
|
@property
|
|
def armed_forces(self) -> ArmedForces:
|
|
return self.game.coalition_for(self.control_point.captured).armed_forces
|
|
|
|
def generate(self) -> bool:
|
|
self.control_point.connected_objectives = []
|
|
self.generate_navy()
|
|
return True
|
|
|
|
def generate_ground_object_from_group(
|
|
self, unit_group: ForceGroup, location: PresetLocation
|
|
) -> None:
|
|
ground_object = unit_group.generate(
|
|
namegen.random_objective_name(),
|
|
location,
|
|
self.control_point,
|
|
self.game,
|
|
)
|
|
self.control_point.connected_objectives.append(ground_object)
|
|
|
|
def generate_navy(self) -> None:
|
|
skip_player_navy = self.generator_settings.no_player_navy
|
|
if self.control_point.captured and skip_player_navy:
|
|
return
|
|
skip_enemy_navy = self.generator_settings.no_enemy_navy
|
|
if not self.control_point.captured 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)
|
|
if not unit_group:
|
|
logging.warning(f"{self.faction_name} has no ForceGroup for Navy")
|
|
return
|
|
self.generate_ground_object_from_group(unit_group, position)
|
|
|
|
|
|
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|
def generate(self) -> bool:
|
|
return True
|
|
|
|
|
|
class GenericCarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|
def update_carrier_name(self, carrier_name: str) -> None:
|
|
# Set Control Point name
|
|
self.control_point.name = carrier_name
|
|
|
|
# Set UnitName. First unit of the TGO is always the carrier
|
|
carrier = next(self.control_point.ground_objects[-1].units)
|
|
carrier.name = carrier_name
|
|
|
|
|
|
class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
|
|
def generate(self) -> bool:
|
|
if not super().generate():
|
|
return False
|
|
|
|
carrier_names = self.faction.carrier_names
|
|
if not carrier_names:
|
|
logging.info(
|
|
f"Skipping generation of {self.control_point.name} because "
|
|
f"{self.faction_name} has no carriers"
|
|
)
|
|
return False
|
|
|
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.AIRCRAFT_CARRIER)
|
|
if not unit_group:
|
|
logging.error(f"{self.faction_name} has no access to AircraftCarrier")
|
|
return False
|
|
self.generate_ground_object_from_group(
|
|
unit_group,
|
|
PresetLocation(
|
|
self.control_point.name,
|
|
self.control_point.position,
|
|
self.control_point.heading,
|
|
),
|
|
)
|
|
self.update_carrier_name(random.choice(carrier_names))
|
|
return True
|
|
|
|
|
|
class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
|
|
def generate(self) -> bool:
|
|
if not super().generate():
|
|
return False
|
|
|
|
lha_names = self.faction.helicopter_carrier_names
|
|
if not lha_names:
|
|
logging.info(
|
|
f"Skipping generation of {self.control_point.name} because "
|
|
f"{self.faction_name} has no LHAs"
|
|
)
|
|
return False
|
|
|
|
unit_group = self.armed_forces.random_group_for_task(
|
|
GroupTask.HELICOPTER_CARRIER
|
|
)
|
|
if not unit_group:
|
|
logging.error(f"{self.faction_name} has no access to HelicopterCarrier")
|
|
return False
|
|
self.generate_ground_object_from_group(
|
|
unit_group,
|
|
PresetLocation(
|
|
self.control_point.name,
|
|
self.control_point.position,
|
|
self.control_point.heading,
|
|
),
|
|
)
|
|
self.update_carrier_name(random.choice(lha_names))
|
|
return True
|
|
|
|
|
|
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|
def __init__(
|
|
self,
|
|
game: Game,
|
|
generator_settings: GeneratorSettings,
|
|
control_point: ControlPoint,
|
|
) -> None:
|
|
super().__init__(game, generator_settings, control_point)
|
|
|
|
def generate(self) -> bool:
|
|
if not super().generate():
|
|
return False
|
|
|
|
self.generate_ground_points()
|
|
return True
|
|
|
|
def generate_ground_points(self) -> None:
|
|
"""Generate ground objects and AA sites for the control point."""
|
|
self.generate_armor_groups()
|
|
self.generate_iads()
|
|
self.generate_scenery_sites()
|
|
self.generate_strike_targets()
|
|
self.generate_offshore_strike_targets()
|
|
self.generate_factories()
|
|
self.generate_ammunition_depots()
|
|
self.generate_missile_sites()
|
|
self.generate_coastal_sites()
|
|
|
|
def get_unit_group_for_task(
|
|
self, position: PresetLocation, task: GroupTask
|
|
) -> Optional[ForceGroup]:
|
|
tgo_config = self.generator_settings.tgo_config
|
|
fg = tgo_config[position.original_name]
|
|
valid_fg = (
|
|
fg
|
|
and task in fg.tasks
|
|
and all([u in self.faction.accessible_units for u in fg.units])
|
|
)
|
|
if valid_fg:
|
|
unit_group = fg
|
|
assert fg
|
|
self.armed_forces.add_or_update_force_group(fg)
|
|
else:
|
|
if fg and not valid_fg:
|
|
logging.warning(
|
|
f"Override in ground_forces failed for {fg} at {position.original_name}"
|
|
)
|
|
unit_group = self.armed_forces.random_group_for_task(task)
|
|
return unit_group
|
|
|
|
def generate_armor_groups(self) -> None:
|
|
for position in self.control_point.preset_locations.armor_groups:
|
|
unit_group = self.get_unit_group_for_task(position, GroupTask.BASE_DEFENSE)
|
|
if not unit_group:
|
|
logging.error(f"{self.faction_name} has no ForceGroup for Armor")
|
|
return
|
|
self.generate_ground_object_from_group(unit_group, position)
|
|
|
|
def generate_aa(self) -> None:
|
|
presets = self.control_point.preset_locations
|
|
aa_tasking = [GroupTask.AAA]
|
|
for position in presets.aaa:
|
|
self.generate_aa_at(position, aa_tasking)
|
|
aa_tasking.insert(0, GroupTask.SHORAD)
|
|
for position in presets.short_range_sams:
|
|
self.generate_aa_at(position, aa_tasking)
|
|
aa_tasking.insert(0, GroupTask.MERAD)
|
|
for position in presets.medium_range_sams:
|
|
self.generate_aa_at(position, aa_tasking)
|
|
aa_tasking.insert(0, GroupTask.LORAD)
|
|
for position in presets.long_range_sams:
|
|
self.generate_aa_at(position, aa_tasking)
|
|
|
|
def generate_ewrs(self) -> None:
|
|
for position in self.control_point.preset_locations.ewrs:
|
|
unit_group = self.armed_forces.random_group_for_task(
|
|
GroupTask.EARLY_WARNING_RADAR
|
|
)
|
|
if not unit_group:
|
|
logging.error(f"{self.faction_name} has no ForceGroup for EWR")
|
|
return
|
|
self.generate_ground_object_from_group(unit_group, position)
|
|
|
|
def generate_building_at(
|
|
self,
|
|
group_task: GroupTask,
|
|
location: PresetLocation,
|
|
) -> None:
|
|
# GroupTask is the type of the building to be generated
|
|
unit_group = self.armed_forces.random_group_for_task(group_task)
|
|
if not unit_group:
|
|
raise RuntimeError(
|
|
f"{self.faction_name} has no access to Building {group_task.description}"
|
|
)
|
|
self.generate_ground_object_from_group(unit_group, location)
|
|
|
|
def generate_ammunition_depots(self) -> None:
|
|
for position in self.control_point.preset_locations.ammunition_depots:
|
|
self.generate_building_at(GroupTask.AMMO, position)
|
|
|
|
def generate_factories(self) -> None:
|
|
for position in self.control_point.preset_locations.factories:
|
|
self.generate_building_at(GroupTask.FACTORY, position)
|
|
|
|
def generate_aa_at(self, location: PresetLocation, tasks: list[GroupTask]) -> None:
|
|
for task in tasks:
|
|
unit_group = self.get_unit_group_for_task(location, task)
|
|
if unit_group:
|
|
# Only take next (smaller) aa_range when no template available for the
|
|
# most requested range. Otherwise break the loop and continue
|
|
self.generate_ground_object_from_group(unit_group, location)
|
|
return
|
|
|
|
logging.error(
|
|
f"{self.faction_name} has no access to SAM {', '.join([task.description for task in tasks])}"
|
|
)
|
|
|
|
def generate_iads(self) -> None:
|
|
# AntiAir
|
|
self.generate_aa()
|
|
# EWR
|
|
self.generate_ewrs()
|
|
# IADS Buildings
|
|
for iads_element in self.control_point.preset_locations.iads_command_center:
|
|
self.generate_building_at(GroupTask.COMMAND_CENTER, iads_element)
|
|
for iads_element in self.control_point.preset_locations.iads_connection_node:
|
|
self.generate_building_at(GroupTask.COMMS, iads_element)
|
|
for iads_element in self.control_point.preset_locations.iads_power_source:
|
|
self.generate_building_at(GroupTask.POWER, iads_element)
|
|
|
|
def generate_scenery_sites(self) -> None:
|
|
presets = self.control_point.preset_locations
|
|
for scenery_group in presets.scenery:
|
|
self.generate_tgo_for_scenery(scenery_group)
|
|
|
|
def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None:
|
|
# Special Handling for scenery Objects based on trigger zones
|
|
iads_role = IadsRole.for_category(scenery.category)
|
|
tgo_type = (
|
|
IadsBuildingGroundObject if iads_role.participate else BuildingGroundObject
|
|
)
|
|
g = tgo_type(
|
|
namegen.random_objective_name(),
|
|
scenery.category,
|
|
PresetLocation(scenery.zone_def.name, scenery.position),
|
|
self.control_point,
|
|
)
|
|
ground_group = TheaterGroup(
|
|
self.game.next_group_id(),
|
|
scenery.zone_def.name,
|
|
PointWithHeading.from_point(scenery.position, Heading.from_degrees(0)),
|
|
[],
|
|
g,
|
|
)
|
|
if iads_role.participate:
|
|
ground_group = IadsGroundGroup.from_group(ground_group)
|
|
ground_group.iads_role = iads_role
|
|
|
|
g.groups.append(ground_group)
|
|
# Each nested trigger zone is a target/building/unit for an objective.
|
|
for zone in scenery.zones:
|
|
zone.name = escape_string_for_lua(zone.name)
|
|
scenery_unit = SceneryUnit(
|
|
zone.id,
|
|
zone.name,
|
|
dcs.statics.Fortification.White_Flag,
|
|
PointWithHeading.from_point(zone.position, Heading.from_degrees(0)),
|
|
g,
|
|
)
|
|
scenery_unit.zone = zone
|
|
ground_group.units.append(scenery_unit)
|
|
|
|
self.control_point.connected_objectives.append(g)
|
|
|
|
def generate_missile_sites(self) -> None:
|
|
for position in self.control_point.preset_locations.missile_sites:
|
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.MISSILE)
|
|
if not unit_group:
|
|
logging.warning(f"{self.faction_name} has no ForceGroup for Missile")
|
|
return
|
|
self.generate_ground_object_from_group(unit_group, position)
|
|
|
|
def generate_coastal_sites(self) -> None:
|
|
for position in self.control_point.preset_locations.coastal_defenses:
|
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.COASTAL)
|
|
if not unit_group:
|
|
logging.warning(f"{self.faction_name} has no ForceGroup for Coastal")
|
|
return
|
|
self.generate_ground_object_from_group(unit_group, position)
|
|
|
|
def generate_strike_targets(self) -> None:
|
|
for position in self.control_point.preset_locations.strike_locations:
|
|
self.generate_building_at(GroupTask.STRIKE_TARGET, position)
|
|
|
|
def generate_offshore_strike_targets(self) -> None:
|
|
for position in self.control_point.preset_locations.offshore_strike_locations:
|
|
self.generate_building_at(GroupTask.OIL, position)
|
|
|
|
|
|
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
|
def generate(self) -> bool:
|
|
if super(FobGroundObjectGenerator, self).generate():
|
|
self.generate_fob()
|
|
return True
|
|
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,
|
|
),
|
|
)
|
|
|
|
|
|
class GroundObjectGenerator:
|
|
def __init__(self, game: Game, generator_settings: GeneratorSettings) -> None:
|
|
self.game = game
|
|
self.generator_settings = generator_settings
|
|
|
|
def generate(self) -> None:
|
|
# Copied so we can remove items from the original list without breaking
|
|
# the iterator.
|
|
control_points = list(self.game.theater.controlpoints)
|
|
for control_point in control_points:
|
|
if not self.generate_for_control_point(control_point):
|
|
self.game.theater.controlpoints.remove(control_point)
|
|
|
|
def generate_for_control_point(self, control_point: ControlPoint) -> bool:
|
|
generator: ControlPointGroundObjectGenerator
|
|
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
|
generator = CarrierGroundObjectGenerator(
|
|
self.game, self.generator_settings, control_point
|
|
)
|
|
elif control_point.cptype == ControlPointType.LHA_GROUP:
|
|
generator = LhaGroundObjectGenerator(
|
|
self.game, self.generator_settings, control_point
|
|
)
|
|
elif isinstance(control_point, OffMapSpawn):
|
|
generator = NoOpGroundObjectGenerator(
|
|
self.game, self.generator_settings, control_point
|
|
)
|
|
elif isinstance(control_point, Fob):
|
|
generator = FobGroundObjectGenerator(
|
|
self.game, self.generator_settings, control_point
|
|
)
|
|
else:
|
|
generator = AirbaseGroundObjectGenerator(
|
|
self.game, self.generator_settings, control_point
|
|
)
|
|
return generator.generate()
|