diff --git a/__init__.py b/__init__.py index bdbeffe1..b63f4a41 100755 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,4 @@ #!/usr/bin/env python3 -import sys - import theater.caucasus import ui.window import ui.mainmenu diff --git a/game/db.py b/game/db.py index 7419ab19..3fb9475b 100644 --- a/game/db.py +++ b/game/db.py @@ -23,6 +23,7 @@ PRICES = { F_15C: 30, M_2000C: 11, + MiG_15bis: 8, MiG_21Bis: 13, MiG_29A: 23, @@ -49,13 +50,14 @@ PRICES = { } UNIT_BY_TASK = { - FighterSweep: [Su_27, Su_33, FA_18C_hornet, F_15C, MiG_21Bis, MiG_29A, F_A_18C, AV8BNA], + FighterSweep: [Su_27, Su_33, FA_18C_hornet, F_15C, MiG_21Bis, MiG_29A, F_A_18C, AV8BNA, ], CAS: [Su_25T, A_10A, A_10C, ], + Transport: [IL_76MD, S_3B_Tanker, ], + AWACS: [E_3A, A_50, ], + CAP: [Armor.MBT_T_90, Armor.MBT_T_80U, Armor.MBT_T_55, Armor.MBT_M1A2_Abrams, Armor.MBT_M60A3_Patton, Armor.ATGM_M1134_Stryker, Armor.APC_BTR_80, ], AirDefence: [AirDefence.AAA_ZU_23_on_Ural_375, AirDefence.SAM_Avenger_M1097 ], - Transport: [IL_76MD, S_3B_Tanker, ], Carriage: [CVN_74_John_C__Stennis, CV_1143_5_Admiral_Kuznetsov, ], - AWACS: [E_3A, A_50, ], } UNIT_BY_COUNTRY = { @@ -88,6 +90,20 @@ UNIT_BY_COUNTRY = { CVN_74_John_C__Stennis], } +PLANE_PAYLOAD_OVERRIDES = { + FA_18C_hornet: { + "*": "AIM-9M*6, AIM-7M*2, FUEL*3", + }, + + MiG_21Bis: { + "*": "Patrol, medium range", + } +} + +PLANE_LIVERY_OVERRIDES = { + FA_18C_hornet: "VFA-34", +} + UnitsDict = typing.Dict[UnitType, int] PlaneDict = typing.Dict[FlyingType, int] ArmorDict = typing.Dict[VehicleType, int] diff --git a/game/event.py b/game/event.py index 4d6ee5fd..2eeda8f8 100644 --- a/game/event.py +++ b/game/event.py @@ -49,10 +49,10 @@ class Event: class GroundInterceptEvent(Event): BONUS_BASE = 3 - TARGET_AMOUNT_FACTOR = 3 - TARGET_VARIETY = 3 + TARGET_AMOUNT_FACTOR = 2 + TARGET_VARIETY = 2 STRENGTH_INFLUENCE = 0.1 - SUCCESS_TARGETS_HIT_PERCENTAGE = 0.7 + SUCCESS_TARGETS_HIT_PERCENTAGE = 0.5 targets = None # type: db.ArmorDict diff --git a/game/game.py b/game/game.py index 845dcffe..1fb3176d 100644 --- a/game/game.py +++ b/game/game.py @@ -83,12 +83,6 @@ class Game: enemy_interception = True break - for to_cp in self.theater.player_points(): - if enemy_interception: - break - - if to_cp.is_global: - continue if to_cp in self.theater.conflicts(False): continue diff --git a/game/operation.py b/game/operation.py index 1aa23ab5..5d960251 100644 --- a/game/operation.py +++ b/game/operation.py @@ -37,6 +37,7 @@ class Operation: self.defender_clients = defender_clients self.from_cp = from_cp self.to_cp = to_cp + self.is_quick = False def initialize(self, mission: Mission, conflict: Conflict): self.mission = mission @@ -46,13 +47,15 @@ class Operation: self.airgen = AircraftConflictGenerator(mission, conflict) self.aagen = AAConflictGenerator(mission, conflict) self.shipgen = ShipGenerator(mission, conflict) - self.envgen = EnvironmentSettingsGenerator(mission, self.game) + self.envgen = EnvironmentSettingsGenerator(mission, conflict, self.game) player_name = self.from_cp.captured and self.attacker_name or self.defender_name enemy_name = self.from_cp.captured and self.defender_name or self.attacker_name self.extra_aagen = ExtraAAConflictGenerator(mission, conflict, self.game, player_name, enemy_name) def prepare(self, is_quick: bool): + self.is_quick = is_quick + if is_quick: self.attackers_starting_position = None self.defenders_starting_position = None @@ -62,7 +65,7 @@ class Operation: def generate(self): self.extra_aagen.generate() - self.envgen.generate() + self.envgen.generate(self.is_quick) def units_of(self, country_name: str) -> typing.Collection[UnitType]: return [] @@ -103,7 +106,6 @@ class CaptureOperation(Operation): mission.country(self.defender_name))) def generate(self): - self.envgen.generate() self.armorgen.generate(self.attack, self.defense) self.aagen.generate(self.aa) @@ -112,6 +114,8 @@ class CaptureOperation(Operation): self.airgen.generate_cas(self.cas, clients=self.attacker_clients, at=self.attackers_starting_position) self.airgen.generate_cas_escort(self.escort, clients=self.attacker_clients, at=self.attackers_starting_position) + super(CaptureOperation, self).generate() + class InterceptOperation(Operation): escort = None # type: db.PlaneDict @@ -144,17 +148,22 @@ class InterceptOperation(Operation): conflict=conflict) def generate(self): - super(InterceptOperation, self).generate() self.airgen.generate_transport(self.transport, self.to_cp.at) self.airgen.generate_transport_escort(self.escort, clients=self.defender_clients) if self.from_cp.is_global: - ship = self.shipgen.generate(type=db.find_unittype(Carriage, self.attacker_name)[0], - at=self.from_cp.at) - self.airgen.generate_interception(self.interceptors, clients=self.attacker_clients, at=ship) + starting_ship = self.shipgen.generate(type=db.find_unittype(Carriage, self.attacker_name)[0], + at=self.from_cp.at) + + if self.is_quick: + starting_ship = None + + self.airgen.generate_interception(self.interceptors, clients=self.attacker_clients, at=starting_ship) else: self.airgen.generate_interception(self.interceptors, clients=self.attacker_clients, at=self.attackers_starting_position) + super(InterceptOperation, self).generate() + class GroundInterceptOperation(Operation): def setup(self, @@ -180,6 +189,7 @@ class GroundInterceptOperation(Operation): conflict=conflict) def generate(self): - super(GroundInterceptOperation, self).generate() self.airgen.generate_cas(self.strikegroup, clients=self.attacker_clients, at=self.attackers_starting_position) self.armorgen.generate({}, self.target) + + super(GroundInterceptOperation, self).generate() diff --git a/gen/aaa.py b/gen/aaa.py index ed4fe0bd..6453d0c3 100644 --- a/gen/aaa.py +++ b/gen/aaa.py @@ -8,6 +8,7 @@ from dcs.mission import * DISTANCE_FACTOR = 4, 5 EXTRA_AA_MIN_DISTANCE = 70000 +EXTRA_AA_POSITION_FROM_CP = 10000 class AAConflictGenerator: def __init__(self, mission: Mission, conflict: Conflict): @@ -37,18 +38,21 @@ class ExtraAAConflictGenerator: self.enemy_name = enemy_name def generate(self): + from theater.conflicttheater import ControlPoint + for cp in self.game.theater.controlpoints: if cp.is_global: continue if cp.position.distance_to_point(self.conflict.position) > EXTRA_AA_MIN_DISTANCE: country_name = cp.captured and self.player_name or self.enemy_name + position = cp.position.point_from_heading(0, EXTRA_AA_POSITION_FROM_CP) self.mission.vehicle_group( country=self.mission.country(country_name), name=namegen.next_ground_group_name(), _type=random.choice(db.find_unittype(AirDefence, country_name)), - position=cp.position, + position=position, group_size=2 ) diff --git a/gen/aircraft.py b/gen/aircraft.py index dc691b32..d619cd72 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -6,19 +6,24 @@ from dcs.mission import * from dcs.unitgroup import * from dcs.unittype import * from dcs.task import * +from dcs.terrain.terrain import NoParkingSlotError SPREAD_DISTANCE_FACTOR = 1, 2 ESCORT_MAX_DIST = 30000 WORKAROUND_WAYP_DIST = 1000 -WARM_START_ALTITUDE = 6000 -WARM_START_AIRSPEED = 300 +WARM_START_ALTITUDE = 3600 +WARM_START_AIRSPEED = 600 +INTERCEPTION_AIRSPEED = 1200 -INTERCEPT_ALT = 15000 -CAS_ALTITUDE = 3000 +TRANSPORT_LANDING_ALT = 500 + +INTERCEPTION_ALT = 3600 +CAS_ALTITUDE = 1000 INTERCEPT_MAX_DISTANCE_FACTOR = 15 + class AircraftConflictGenerator: escort_targets = [] # type: typing.List[PlaneGroup] @@ -34,6 +39,39 @@ class AircraftConflictGenerator: ) return point.random_point_within(distance, self.conflict.size * SPREAD_DISTANCE_FACTOR[0]) + def _split_to_groups(self, dict: db.PlaneDict, clients: db.PlaneDict = None) -> typing.Collection[typing.Tuple[FlyingType, int, int]]: + for flying_type, count in dict.items(): + if clients: + client_count = clients.get(flying_type, 0) + else: + client_count = 0 + + while count > 0: + group_size = min(count, 4) + client_size = max(min(client_count, 4), 0) + + yield (flying_type, group_size, client_size) + count -= group_size + client_count -= client_size + + def _setup_group(self, group: FlyingGroup, for_task: Task): + did_load_loadout = False + unit_type = group.units[0].unit_type + if unit_type in db.PLANE_PAYLOAD_OVERRIDES: + if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]: + group.load_loadout(db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task]) + did_load_loadout = True + elif "*" in db.PLANE_PAYLOAD_OVERRIDES[unit_type]: + group.load_loadout(db.PLANE_PAYLOAD_OVERRIDES[unit_type]["*"]) + did_load_loadout = True + + if not did_load_loadout: + group.load_task_default_loadout(for_task) + + if unit_type in db.PLANE_LIVERY_OVERRIDES: + for unit_instance in group.units: + unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type] + def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, airport: Airport = None) -> FlyingGroup: assert count > 0 assert unit is not None @@ -98,7 +136,10 @@ class AircraftConflictGenerator: elif isinstance(at, ShipGroup): return self._generate_at_carrier(name, side, unit_type, count, client_count, at) elif issubclass(at, Airport): - return self._generate_at_airport(name, side, unit_type, count, client_count, at) + try: + return self._generate_at_airport(name, side, unit_type, count, client_count, at) + except NoParkingSlotError: + return self._generate_inflight(name, side, unit_type, count, client_count, at.position) else: assert False @@ -106,41 +147,42 @@ class AircraftConflictGenerator: if len(self.escort_targets) == 0: return - for type, count in units.items(): + for flying_type, count, client_count in self._split_to_groups(units, clients): group = self._generate_group( name=namegen.next_escort_group_name(), side=side, - unit_type=type, + unit_type=flying_type, count=count, - client_count=clients.get(type, 0), + client_count=client_count, at=at) group.task = Escort.name - group.load_task_default_loadout(dcs.task.Escort) heading = group.position.heading_between_point(self.conflict.position) position = group.position # type: Point - wayp = group.add_waypoint(position.point_from_heading(heading, WORKAROUND_WAYP_DIST), CAS_ALTITUDE) + wayp = group.add_waypoint(position.point_from_heading(heading, WORKAROUND_WAYP_DIST), CAS_ALTITUDE, WARM_START_AIRSPEED) for group in self.escort_targets: wayp.tasks.append(EscortTaskAction(group.id, engagement_max_dist=ESCORT_MAX_DIST)) + self._setup_group(group, dcs.task.Escort) + def generate_cas(self, attackers: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None): assert len(self.escort_targets) == 0 - for type, count in attackers.items(): + for flying_type, count, client_count in self._split_to_groups(attackers, clients): group = self._generate_group( name=namegen.next_cas_group_name(), side=self.conflict.attackers_side, - unit_type=type, + unit_type=flying_type, count=count, - client_count=clients.get(type, 0), + client_count=client_count, at=at and at or self._group_point(self.conflict.air_attackers_location)) self.escort_targets.append(group) - group.add_waypoint(self.conflict.position, CAS_ALTITUDE) + group.add_waypoint(self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED) group.task = CAS.name - group.load_task_default_loadout(CAS) + self._setup_group(group, CAS) def generate_cas_escort(self, attackers: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None): self._generate_escort( @@ -157,54 +199,55 @@ class AircraftConflictGenerator: at=at and at or self._group_point(self.conflict.air_defenders_location)) def generate_defense(self, defenders: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None): - for type, count in defenders.items(): + for flying_type, count, client_count in self._split_to_groups(defenders, clients): group = self._generate_group( name=namegen.next_intercept_group_name(), side=self.conflict.defenders_side, - unit_type=type, + unit_type=flying_type, count=count, - client_count=clients.get(type, 0), + client_count=client_count, at=at and at or self._group_point(self.conflict.air_defenders_location)) group.task = FighterSweep.name - group.load_task_default_loadout(FighterSweep) - wayp = group.add_waypoint(self.conflict.position, CAS_ALTITUDE) + wayp = group.add_waypoint(self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED) wayp.tasks.append(dcs.task.EngageTargets(max_distance=self.conflict.size * INTERCEPT_MAX_DISTANCE_FACTOR)) wayp.tasks.append(dcs.task.OrbitAction()) + self._setup_group(group, FighterSweep) def generate_transport(self, transport: db.PlaneDict, destination: Airport): assert len(self.escort_targets) == 0 - for type, count in transport.items(): + for flying_type, count, client_count in self._split_to_groups(transport): group = self._generate_group( name=namegen.next_transport_group_name(), side=self.conflict.defenders_side, - unit_type=type, + unit_type=flying_type, count=count, - client_count=0, + client_count=client_count, at=self._group_point(self.conflict.air_defenders_location)) group.task = Transport.name self.escort_targets.append(group) + group.add_waypoint(destination.position.random_point_within(0, 0), TRANSPORT_LANDING_ALT) group.land_at(destination) def generate_interception(self, interceptors: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None): - for type, count in interceptors.items(): + for flying_type, count, client_count in self._split_to_groups(interceptors, clients): group = self._generate_group( name=namegen.next_intercept_group_name(), side=self.conflict.attackers_side, - unit_type=type, + unit_type=flying_type, count=count, - client_count=clients.get(type, 0), + client_count=client_count, at=at and at or self._group_point(self.conflict.air_attackers_location)) group.task = FighterSweep.name - group.load_task_default_loadout(FighterSweep) heading = group.position.heading_between_point(self.conflict.position) - initial_wayp = group.add_waypoint(group.position.point_from_heading(heading, WORKAROUND_WAYP_DIST), INTERCEPT_ALT) + initial_wayp = group.add_waypoint(group.position.point_from_heading(heading, WORKAROUND_WAYP_DIST), INTERCEPTION_ALT, INTERCEPTION_AIRSPEED) initial_wayp.tasks.append(EngageTargets()) wayp = group.add_waypoint(self.conflict.position, 0) wayp.tasks.append(EngageTargets()) + self._setup_group(group, FighterSweep) diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 4a4e6897..83217068 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -18,13 +18,14 @@ def _opposite_heading(h): return h+180 GROUND_DISTANCE_FACTOR = 2 -AIR_DISTANCE = 8000 +AIR_DISTANCE = 32000 INTERCEPT_ATTACKERS_HEADING = -45, 45 INTERCEPT_DEFENDERS_HEADING = -10, 10 INTERCEPT_ATTACKERS_DISTANCE = 60000 INTERCEPT_DEFENDERS_DISTANCE = 30000 -INTERCEPT_MAX_DISTANCE = 45000 +INTERCEPT_MAX_DISTANCE = 80000 +INTERCEPT_MIN_DISTANCE = 45000 class Conflict: @@ -62,7 +63,8 @@ class Conflict: from theater.conflicttheater import ALL_RADIALS heading = from_cp.position.heading_between_point(to_cp.position) - distance = min(from_cp.position.distance_to_point(to_cp.position) / 2, INTERCEPT_MAX_DISTANCE) + raw_distance = from_cp.position.distance_to_point(to_cp.position) / 2 + distance = max(min(raw_distance, INTERCEPT_MAX_DISTANCE), INTERCEPT_MIN_DISTANCE) position = from_cp.position.point_from_heading(heading, distance) instance = self() @@ -74,7 +76,7 @@ class Conflict: instance.radials = ALL_RADIALS instance.air_attackers_location = instance.position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, INTERCEPT_ATTACKERS_DISTANCE) - instance.air_defenders_location = instance.position.point_from_heading(random.randint(*INTERCEPT_DEFENDERS_HEADING) + heading, INTERCEPT_DEFENDERS_DISTANCE) + instance.air_defenders_location = instance.position return instance diff --git a/gen/envsettingsgen.py b/gen/envsettingsgen.py index 9987f91a..e923a62c 100644 --- a/gen/envsettingsgen.py +++ b/gen/envsettingsgen.py @@ -2,9 +2,15 @@ import typing import random from dcs.mission import Mission +from dcs.triggers import * +from dcs.condition import * +from dcs.action import * from theater.weatherforecast import WeatherForecast +from theater.conflicttheater import Conflict +ACTIVATION_TRIGGER_SIZE = 80000 +ACTIVATION_TRIGGER_MIN_DISTANCE = 5000 RANDOM_TIME = { "night": 0, @@ -22,11 +28,12 @@ RANDOM_WEATHER = { } class EnvironmentSettingsGenerator: - def __init__(self, mission: Mission, game): + def __init__(self, mission: Mission, conflict: Conflict, game): self.mission = mission + self.conflict = conflict self.game = game - def generate(self): + def generate(self, is_quick: bool): time_roll = random.randint(0, 100) time_period = None for k, v in RANDOM_TIME.items(): @@ -53,10 +60,34 @@ class EnvironmentSettingsGenerator: elif weather_type == 3: self.mission.weather.random(self.mission.start_time, self.mission.terrain) + player_coalition = self.game.player == "USA" and "blue" or "red" + enemy_coalition = player_coalition == "blue" and "red" or "blue" + for cp in self.game.theater.controlpoints: if cp.is_global: continue - - player_coalition = self.game.player == "USA" and "blue" or "red" - enemy_coalition = player_coalition == "blue" and "red" or "blue" self.mission.terrain.airport_by_id(cp.at.id).set_coalition(cp.captured and player_coalition or enemy_coalition) + + if not is_quick: + activate_by_trigger = [] + for coalition_name, coalition in self.mission.coalition.items(): + for country in coalition.countries.values(): + if coalition_name == enemy_coalition: + for plane_group in country.plane_group: + plane_group.late_activation = True + activate_by_trigger.append(plane_group) + + for vehicle_group in country.vehicle_group: + vehicle_group.late_activation = True + activate_by_trigger.append(vehicle_group) + + zone_distance_to_aircraft = self.conflict.air_attackers_location.distance_to_point(self.conflict.position) + zone_size = min(zone_distance_to_aircraft - ACTIVATION_TRIGGER_MIN_DISTANCE, ACTIVATION_TRIGGER_SIZE) + + activation_trigger_zone = self.mission.triggers.add_triggerzone(self.conflict.position, zone_size) + activation_trigger = TriggerOnce(Event.NoEvent, "Activation trigger") + activation_trigger.add_condition(PartOfCoalitionInZone(player_coalition, activation_trigger_zone.id)) + for group in activate_by_trigger: + activation_trigger.add_action(ActivateGroup(group.id)) + + self.mission.triggerrules.triggers.append(activation_trigger) diff --git a/theater/controlpoint.py b/theater/controlpoint.py index 72bbf0f7..37f78c4e 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -62,10 +62,8 @@ class ControlPoint: return closest_radial def conflict_attack(self, from_cp, attacker: Country, defender: Country) -> Conflict: - cp = from_cp # type: ControlPoint - - attack_radial = self.find_radial(cp.position.heading_between_point(self.position)) - defense_radial = self.find_radial(self.position.heading_between_point(cp.position), ignored_radial=attack_radial) + attack_radial = self.find_radial(self.position.heading_between_point(from_cp.position)) + defense_radial = self.find_radial(from_cp.position.heading_between_point(self.position), ignored_radial=attack_radial) return Conflict.capture_conflict(attacker=attacker, attack_heading=attack_radial, diff --git a/ui/eventmenu.py b/ui/eventmenu.py index e2cf8920..460166e8 100644 --- a/ui/eventmenu.py +++ b/ui/eventmenu.py @@ -17,6 +17,11 @@ class EventMenu(Menu): self.armor_scramble_entries = {} self.aircraft_client_entries = {} + if self.event.attacker_name == self.game.player: + self.base = self.event.from_cp.base + else: + self.base = self.event.to_cp.base + self.frame = self.window.right_pane def display(self): @@ -55,28 +60,22 @@ class EventMenu(Menu): row += 1 - base = None # type: Base - if self.event.attacker_name == self.game.player: - base = self.event.from_cp.base - else: - base = self.event.to_cp.base - label("Aircraft") label("Amount", row, 1) label("Client slots", row, 2) row += 1 - for unit_type, count in base.aircraft.items(): + for unit_type, count in self.base.aircraft.items(): scrable_row(unit_type, count) - if not base.total_planes: + if not self.base.total_planes: label("None") label("Armor") - for unit_type, count in base.armor.items(): + for unit_type, count in self.base.armor.items(): scramble_armor_row(unit_type, count) - if not base.total_armor: + if not self.base.total_armor: label("None") Button(self.frame, text="Commit", command=self.start).grid(column=0, row=row) @@ -89,6 +88,7 @@ class EventMenu(Menu): for unit_type, field in self.aircraft_scramble_entries.items(): value = field.get() if value and int(value) > 0: + #amount = min(int(value), self.base.aircraft[unit_type]) amount = int(value) task = db.unit_task(unit_type) diff --git a/userdata/debriefing.py b/userdata/debriefing.py index ac6c2360..7cd9d60b 100644 --- a/userdata/debriefing.py +++ b/userdata/debriefing.py @@ -72,6 +72,11 @@ class Debriefing: enemy.name: calculate_losses(enemy_units, self.alive_units.get(enemy.id, {})), } + self.alive_units = { + player.name: self.alive_units.get(player.id, {}), + enemy.name: self.alive_units.get(enemy.id, {}), + } + def debriefing_directory_location() -> str: return "build/debriefing"