mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
- Fixes ferry flights causing errors when "Nothing" is not available - Logs a warning when a frontline stance is suddenly no longer available and uses defense stance as fallback which used to be the default. An investigation is still required to determine the cause of this...
179 lines
6.5 KiB
Python
179 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import random
|
|
from enum import Enum
|
|
from typing import List, TYPE_CHECKING
|
|
from uuid import UUID
|
|
|
|
from game.data.units import UnitClass
|
|
from game.dcs.groundunittype import GroundUnitType
|
|
from .combat_stance import CombatStance
|
|
|
|
if TYPE_CHECKING:
|
|
from game import Game
|
|
from game.theater import ControlPoint
|
|
|
|
MAX_COMBAT_GROUP_PER_CP = 10
|
|
|
|
|
|
class CombatGroupRole(Enum):
|
|
TANK = 1
|
|
APC = 2
|
|
IFV = 3
|
|
ARTILLERY = 4
|
|
SHORAD = 5
|
|
LOGI = 6
|
|
INFANTRY = 7
|
|
ATGM = 8
|
|
RECON = 9
|
|
|
|
|
|
DISTANCE_FROM_FRONTLINE = {
|
|
CombatGroupRole.TANK: (2200, 3200),
|
|
CombatGroupRole.APC: (2700, 3700),
|
|
CombatGroupRole.IFV: (2700, 3700),
|
|
CombatGroupRole.ARTILLERY: (16000, 18000),
|
|
CombatGroupRole.SHORAD: (5000, 8000),
|
|
CombatGroupRole.LOGI: (18000, 20000),
|
|
CombatGroupRole.INFANTRY: (2800, 3300),
|
|
CombatGroupRole.ATGM: (5200, 6200),
|
|
CombatGroupRole.RECON: (2000, 3000),
|
|
}
|
|
|
|
GROUP_SIZES_BY_COMBAT_STANCE = {
|
|
CombatStance.DEFENSIVE: [2, 4, 6],
|
|
CombatStance.AGGRESSIVE: [2, 4, 6],
|
|
CombatStance.RETREAT: [2, 4, 6, 8],
|
|
CombatStance.BREAKTHROUGH: [4, 6, 6, 8],
|
|
CombatStance.ELIMINATION: [2, 4, 4, 4, 6],
|
|
CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4],
|
|
}
|
|
|
|
|
|
class CombatGroup:
|
|
def __init__(
|
|
self, role: CombatGroupRole, unit_type: GroundUnitType, size: int
|
|
) -> None:
|
|
self.unit_type = unit_type
|
|
self.size = size
|
|
self.role = role
|
|
self.start_position = None
|
|
|
|
def __str__(self) -> str:
|
|
s = f"ROLE : {self.role}\n"
|
|
if self.size:
|
|
s += f"UNITS {self.unit_type} * {self.size}"
|
|
return s
|
|
|
|
|
|
class GroundPlanner:
|
|
def __init__(self, cp: ControlPoint, game: Game) -> None:
|
|
self.cp = cp
|
|
self.game = game
|
|
self.connected_enemy_cp = [
|
|
cp for cp in self.cp.connected_points if cp.captured != self.cp.captured
|
|
]
|
|
self.tank_groups: List[CombatGroup] = []
|
|
self.apc_group: List[CombatGroup] = []
|
|
self.ifv_group: List[CombatGroup] = []
|
|
self.art_group: List[CombatGroup] = []
|
|
self.atgm_group: List[CombatGroup] = []
|
|
self.logi_groups: List[CombatGroup] = []
|
|
self.shorad_groups: List[CombatGroup] = []
|
|
self.recon_groups: List[CombatGroup] = []
|
|
|
|
self.units_per_cp: dict[UUID, List[CombatGroup]] = {}
|
|
for cp in self.connected_enemy_cp:
|
|
self.units_per_cp[cp.id] = []
|
|
self.reserve: List[CombatGroup] = []
|
|
|
|
def plan_groundwar(self) -> None:
|
|
ground_unit_limit = self.cp.frontline_unit_count_limit
|
|
|
|
remaining_available_frontline_units = ground_unit_limit
|
|
# Now applies the ratio between ground unit limit and the total number of ground units to each unit type
|
|
# when planning the ground war. This will help with monocultures of certain unit types when the control
|
|
# point has more units than can be spawned in one mission. In short, this will make more unit types to spawn.
|
|
if self.cp.base.total_armor > 0:
|
|
ratio_of_frontline_units_to_reserves = min(
|
|
ground_unit_limit / self.cp.base.total_armor, 1
|
|
)
|
|
else:
|
|
ratio_of_frontline_units_to_reserves = 1
|
|
|
|
# Create combat groups and assign them randomly to each enemy CP
|
|
for unit_type in self.cp.base.armor:
|
|
unit_class = unit_type.unit_class
|
|
if unit_class is UnitClass.TANK:
|
|
collection = self.tank_groups
|
|
role = CombatGroupRole.TANK
|
|
elif unit_class is UnitClass.APC:
|
|
collection = self.apc_group
|
|
role = CombatGroupRole.APC
|
|
elif unit_class is UnitClass.ARTILLERY:
|
|
collection = self.art_group
|
|
role = CombatGroupRole.ARTILLERY
|
|
elif unit_class is UnitClass.IFV:
|
|
collection = self.ifv_group
|
|
role = CombatGroupRole.IFV
|
|
elif unit_class is UnitClass.LOGISTICS:
|
|
collection = self.logi_groups
|
|
role = CombatGroupRole.LOGI
|
|
elif unit_class is UnitClass.ATGM:
|
|
collection = self.atgm_group
|
|
role = CombatGroupRole.ATGM
|
|
elif unit_class in [UnitClass.SHORAD, UnitClass.AAA]:
|
|
collection = self.shorad_groups
|
|
role = CombatGroupRole.SHORAD
|
|
elif unit_class is UnitClass.RECON:
|
|
collection = self.recon_groups
|
|
role = CombatGroupRole.RECON
|
|
else:
|
|
logging.warning(
|
|
f"Unused front line vehicle at base {unit_type}: unknown unit class"
|
|
)
|
|
continue
|
|
|
|
available = (
|
|
self.cp.base.armor[unit_type] * ratio_of_frontline_units_to_reserves
|
|
)
|
|
if 0 < available < 1:
|
|
available = 1
|
|
|
|
# Round the number of units to an integer
|
|
if available > remaining_available_frontline_units:
|
|
available = remaining_available_frontline_units
|
|
available = round(available)
|
|
|
|
remaining_available_frontline_units -= available
|
|
|
|
while available > 0:
|
|
if len(self.connected_enemy_cp) > 0:
|
|
enemy_cp: ControlPoint = random.choice(self.connected_enemy_cp)
|
|
frontline_stance = self.cp.stances.get(enemy_cp.id)
|
|
if not frontline_stance:
|
|
logging.warning(
|
|
f"{self.cp.name} lost its frontline stance for {enemy_cp.name}"
|
|
)
|
|
frontline_stance = CombatStance.DEFENSIVE
|
|
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[frontline_stance]
|
|
if role == CombatGroupRole.SHORAD:
|
|
count = 1
|
|
else:
|
|
choices = [s for s in group_size_choice if s <= available]
|
|
if not choices:
|
|
choices.append(1)
|
|
count = random.choice(choices)
|
|
|
|
available -= count
|
|
group = CombatGroup(role, unit_type, count)
|
|
self.units_per_cp[enemy_cp.id].append(group)
|
|
else:
|
|
group = CombatGroup(role, unit_type, available)
|
|
self.reserve.append(CombatGroup(role, unit_type, available))
|
|
collection.append(group)
|
|
|
|
if remaining_available_frontline_units == 0:
|
|
break
|