diff --git a/game/event/__init__.py b/game/event/__init__.py index f9a147a6..11025c6a 100644 --- a/game/event/__init__.py +++ b/game/event/__init__.py @@ -5,5 +5,6 @@ from .intercept import * from .baseattack import * from .navalintercept import * from .insurgentattack import * +from .convoystrike import * from .infantrytransport import * from .strike import * diff --git a/game/event/convoystrike.py b/game/event/convoystrike.py new file mode 100644 index 00000000..e7418769 --- /dev/null +++ b/game/event/convoystrike.py @@ -0,0 +1,79 @@ +import math +import random + +from dcs.task import * + +from game import * +from game.event import * +from game.event.frontlineattack import FrontlineAttackEvent + +from .event import * +from game.operation.convoystrike import ConvoyStrikeOperation + +TRANSPORT_COUNT = 4, 6 +DEFENDERS_AMOUNT_FACTOR = 4 + + +class ConvoyStrikeEvent(Event): + SUCCESS_FACTOR = 0.7 + STRENGTH_INFLUENCE = 0.25 + + targets = None # type: db.ArmorDict + + @property + def threat_description(self): + return "" + + @property + def tasks(self): + return [CAS] + + def flight_name(self, for_task: typing.Type[Task]) -> str: + if for_task == CAS: + return "Strike flight" + + def __str__(self): + return "Convoy Strike" + + def skip(self): + self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) + + def commit(self, debriefing: Debriefing): + super(ConvoyStrikeEvent, self).commit(debriefing) + + if self.from_cp.captured: + if self.is_successfull(debriefing): + self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) + else: + if self.is_successfull(debriefing): + self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) + + def is_successfull(self, debriefing: Debriefing): + killed_units = sum([v for k, v in debriefing.destroyed_units[self.defender_name].items() if db.unit_task(k) in [PinpointStrike, Reconnaissance]]) + all_units = sum(self.targets.values()) + attackers_success = (float(killed_units) / (all_units + 0.01)) > self.SUCCESS_FACTOR + if self.from_cp.captured: + return attackers_success + else: + return not attackers_success + + def player_attacking(self, flights: db.TaskForceDict): + assert CAS in flights and len(flights) == 1, "Invalid flights" + + convoy_unittype = db.find_unittype(Reconnaissance, self.defender_name)[0] + defense_unittype = db.find_unittype(PinpointStrike, self.defender_name)[0] + + defenders_count = int(math.ceil(self.from_cp.base.strength * self.from_cp.importance * DEFENDERS_AMOUNT_FACTOR)) + self.targets = {convoy_unittype: random.randrange(*TRANSPORT_COUNT), + defense_unittype: defenders_count, } + + op = ConvoyStrikeOperation(game=self.game, + attacker_name=self.attacker_name, + defender_name=self.defender_name, + from_cp=self.from_cp, + departure_cp=self.departure_cp, + to_cp=self.to_cp) + op.setup(target=self.targets, + strikegroup=flights[CAS]) + + self.operation = op diff --git a/game/event/insurgentattack.py b/game/event/insurgentattack.py index bf4d373c..e98b1e7f 100644 --- a/game/event/insurgentattack.py +++ b/game/event/insurgentattack.py @@ -39,7 +39,7 @@ class InsurgentAttackEvent(Event): killed_units = sum([v for k, v in debriefing.destroyed_units[self.attacker_name].items() if db.unit_task(k) == PinpointStrike]) all_units = sum(self.targets.values()) attackers_success = (float(killed_units) / (all_units + 0.01)) > self.SUCCESS_FACTOR - if self.departure_cp.captured: + if self.from_cp.captured: return attackers_success else: return not attackers_success diff --git a/game/game.py b/game/game.py index d177271b..6b4502f0 100644 --- a/game/game.py +++ b/game/game.py @@ -56,6 +56,7 @@ EVENT_PROBABILITIES = { # events randomly present; only for the player InfantryTransportEvent: [25, 0], + ConvoyStrikeEvent: [25, 0], # events conditionally present; for both enemy and player BaseAttackEvent: [100, 9], @@ -164,7 +165,7 @@ class Game: def _generate_events(self): for player_cp, enemy_cp in self.theater.conflicts(True): for event_class, (player_probability, enemy_probability) in EVENT_PROBABILITIES.items(): - if event_class in [FrontlineAttackEvent, FrontlinePatrolEvent, InfantryTransportEvent]: + if event_class in [FrontlineAttackEvent, FrontlinePatrolEvent, InfantryTransportEvent, ConvoyStrikeEvent]: # skip events requiring frontline if not Conflict.has_frontline_between(player_cp, enemy_cp): continue diff --git a/game/operation/convoystrike.py b/game/operation/convoystrike.py new file mode 100644 index 00000000..5cb76f55 --- /dev/null +++ b/game/operation/convoystrike.py @@ -0,0 +1,46 @@ +from game.db import assigned_units_split + +from .operation import * + + +class ConvoyStrikeOperation(Operation): + strikegroup = None # type: db.AssignedUnitsDict + target = None # type: db.ArmorDict + + def setup(self, + target: db.ArmorDict, + strikegroup: db.AssignedUnitsDict): + self.strikegroup = strikegroup + self.target = target + + def prepare(self, terrain: Terrain, is_quick: bool): + super(ConvoyStrikeOperation, self).prepare(terrain, is_quick) + + conflict = Conflict.convoy_strike_conflict( + attacker=self.current_mission.country(self.attacker_name), + defender=self.current_mission.country(self.defender_name), + from_cp=self.from_cp, + to_cp=self.to_cp, + theater=self.game.theater + ) + + self.initialize(mission=self.current_mission, + conflict=conflict) + + def generate(self): + planes_flights = {k: v for k, v in self.strikegroup.items() if k in plane_map.values()} + self.airgen.generate_cas_strikegroup(*assigned_units_split(planes_flights), at=self.attackers_starting_position) + + heli_flights = {k: v for k, v in self.strikegroup.items() if k in helicopters.helicopter_map.values()} + if heli_flights: + self.briefinggen.append_frequency("FARP + Heli flights", "127.5 MHz AM") + for farp, dict in zip(self.groundobjectgen.generate_farps(sum([x[0] for x in heli_flights.values()])), + db.assignedunits_split_to_count(heli_flights, self.groundobjectgen.FARP_CAPACITY)): + self.airgen.generate_cas_strikegroup(*assigned_units_split(dict), + at=farp, + escort=len(planes_flights) == 0) + + self.armorgen.generate_convoy(self.target) + + self.briefinggen.append_waypoint("TARGET") + super(ConvoyStrikeOperation, self).generate() diff --git a/gen/armor.py b/gen/armor.py index 070da766..92d1d89a 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -36,7 +36,7 @@ class ArmorConflictGenerator: return point.random_point_within(distance, self.conflict.size * SPREAD_DISTANCE_SIZE_FACTOR) - def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point, to: Point = None): + def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point, to: Point = None, move_formation: PointAction = PointAction.OffRoad): for c in range(count): logging.info("armorgen: {} for {}".format(unit, side.id)) group = self.m.vehicle_group( @@ -45,7 +45,7 @@ class ArmorConflictGenerator: unit, position=self._group_point(at), group_size=1, - move_formation=PointAction.OffRoad) + move_formation=move_formation) vehicle: Vehicle = group.units[0] vehicle.player_can_drive = True @@ -53,7 +53,7 @@ class ArmorConflictGenerator: if not to: to = self.conflict.position.point_from_heading(0, 500) - wayp = group.add_waypoint(self._group_point(to)) + wayp = group.add_waypoint(self._group_point(to), move_formation=move_formation) wayp.tasks = [] def _generate_fight_at(self, attackers: db.ArmorDict, defenders: db.ArmorDict, position: Point): @@ -109,6 +109,16 @@ class ArmorConflictGenerator: random.randint(0, self.conflict.distance)) self._generate_fight_at(attacker_group_dict, target_group_dict, position) + def generate_convoy(self, units: db.ArmorDict): + for type, count in units.items(): + self._generate_group( + side=self.conflict.defenders_side, + unit=type, + count=count, + at=self.conflict.ground_defenders_location, + to=self.conflict.position, + move_formation=PointAction.OnRoad) + def generate_passengers(self, count: int): unit_type = random.choice(db.find_unittype(Nothing, self.conflict.attackers_side.name)) diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 63336147..bf169aaf 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -321,6 +321,31 @@ class Conflict: air_defenders_location=position.point_from_heading(heading, AIR_DISTANCE), ) + @classmethod + def convoy_strike_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): + frontline_position, frontline_heading, frontline_length = Conflict.frontline_vector(from_cp, to_cp, theater) + if not frontline_position: + assert False + + heading = _heading_sum(frontline_heading, +45) + starting_position = Conflict._find_ground_position(frontline_position.point_from_heading(heading, 15000), + GROUND_INTERCEPT_SPREAD, + _opposite_heading(heading), theater) + destination_position = frontline_position + + return cls( + position=destination_position, + theater=theater, + from_cp=from_cp, + to_cp=to_cp, + attackers_side=attacker, + defenders_side=defender, + ground_attackers_location=None, + ground_defenders_location=starting_position, + air_attackers_location=starting_position.point_from_heading(_opposite_heading(heading), AIR_DISTANCE), + air_defenders_location=starting_position.point_from_heading(heading, AIR_DISTANCE), + ) + @classmethod def frontline_cas_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): assert cls.has_frontline_between(from_cp, to_cp) diff --git a/resources/tools/generate_loadout_check.py b/resources/tools/generate_loadout_check.py index 575b8519..33bce161 100644 --- a/resources/tools/generate_loadout_check.py +++ b/resources/tools/generate_loadout_check.py @@ -1,4 +1,5 @@ import os +import sys import dcs from game import db diff --git a/resources/ui/events/air_intercept.png b/resources/ui/events/air_intercept.png new file mode 100644 index 00000000..1b66f0f6 Binary files /dev/null and b/resources/ui/events/air_intercept.png differ diff --git a/resources/ui/events/attack.PNG b/resources/ui/events/attack.PNG new file mode 100644 index 00000000..eb7169bf Binary files /dev/null and b/resources/ui/events/attack.PNG differ diff --git a/resources/ui/events/capture.PNG b/resources/ui/events/capture.PNG new file mode 100644 index 00000000..98ab3cd9 Binary files /dev/null and b/resources/ui/events/capture.PNG differ diff --git a/resources/ui/events/convoy.png b/resources/ui/events/convoy.png new file mode 100644 index 00000000..f2e31f6e Binary files /dev/null and b/resources/ui/events/convoy.png differ diff --git a/resources/ui/events/delivery.PNG b/resources/ui/events/delivery.PNG new file mode 100644 index 00000000..3a291616 Binary files /dev/null and b/resources/ui/events/delivery.PNG differ diff --git a/resources/ui/events/infantry.PNG b/resources/ui/events/infantry.PNG new file mode 100644 index 00000000..a3dd2b38 Binary files /dev/null and b/resources/ui/events/infantry.PNG differ diff --git a/resources/ui/events/insurgent_attack.PNG b/resources/ui/events/insurgent_attack.PNG new file mode 100644 index 00000000..44f7ea62 Binary files /dev/null and b/resources/ui/events/insurgent_attack.PNG differ diff --git a/resources/ui/events/naval_intercept.PNG b/resources/ui/events/naval_intercept.PNG new file mode 100644 index 00000000..d121b471 Binary files /dev/null and b/resources/ui/events/naval_intercept.PNG differ diff --git a/resources/ui/events/strike.PNG b/resources/ui/events/strike.PNG new file mode 100644 index 00000000..73eb2c67 Binary files /dev/null and b/resources/ui/events/strike.PNG differ diff --git a/ui/overviewcanvas.py b/ui/overviewcanvas.py index c680bbae..a3b3e9db 100644 --- a/ui/overviewcanvas.py +++ b/ui/overviewcanvas.py @@ -162,6 +162,7 @@ class OverviewCanvas: FrontlineAttackEvent: "attack", InfantryTransportEvent: "infantry", InsurgentAttackEvent: "insurgent_attack", + ConvoyStrikeEvent: "convoy", InterceptEvent: "air_intercept", NavalInterceptEvent: "naval_intercept", StrikeEvent: "strike", @@ -486,7 +487,7 @@ class OverviewCanvas: label_to_draw = None for event in self.game.events: location = event.location - if isinstance(event, FrontlinePatrolEvent) or isinstance(event, FrontlineAttackEvent): + if type(event) in [FrontlineAttackEvent, FrontlinePatrolEvent, ConvoyStrikeEvent]: location = self._frontline_center(event.from_cp, event.to_cp) rect = _location_to_rect(location)