diff --git a/__init__.py b/__init__.py index 927f9d98..0579cdea 100755 --- a/__init__.py +++ b/__init__.py @@ -17,10 +17,13 @@ from game.game import Game from theater import start_generator from userdata import persistency, logging as logging_module +assert len(sys.argv) == 3, "__init__.py should be started with two mandatory arguments: %UserProfile% location and application version" + persistency.setup(sys.argv[1]) dcs.planes.FlyingType.payload_dirs = [os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources\\payloads")] -logging_module.setup_version_string(sys.argv[2]) +VERSION_STRING = sys.argv[2] +logging_module.setup_version_string(VERSION_STRING) logging.info("Using {} as userdata folder".format(persistency.base_path())) @@ -29,10 +32,20 @@ def proceed_to_main_menu(game: Game): m.display() +def is_version_compatible(save_version): + current_version = VERSION_STRING.split(".") + save_version = save_version.split(".") + + if current_version[:2] == save_version[:2]: + return True + + return False + + w = ui.window.Window() try: game = persistency.restore_game() - if not game: + if not game or not is_version_compatible(game.settings.version): new_game_menu = None # type: NewGameMenu def start_new_game(player_name: str, enemy_name: str, terrain: str, sams: bool, midgame: bool, multiplier: float): @@ -54,6 +67,7 @@ try: game.budget = int(game.budget * multiplier) game.settings.multiplier = multiplier game.settings.sams = sams + game.settings.version = VERSION_STRING if midgame: game.budget = game.budget * 4 * len(list(conflicttheater.conflicts())) diff --git a/game/db.py b/game/db.py index 0b63cf39..374b58d8 100644 --- a/game/db.py +++ b/game/db.py @@ -100,7 +100,6 @@ PRICES = { AirDefence.AAA_Vulcan_M163: 5, AirDefence.SAM_Avenger_M1097: 10, - AirDefence.SAM_Patriot_ICC: 15, AirDefence.AAA_ZU_23_on_Ural_375: 5, AirDefence.SAM_SA_18_Igla_S_MANPADS: 8, @@ -185,7 +184,6 @@ UNIT_BY_TASK = { AirDefence.SAM_Avenger_M1097, AirDefence.SAM_Avenger_M1097, AirDefence.SAM_Avenger_M1097, - AirDefence.SAM_Patriot_ICC, AirDefence.AAA_ZU_23_on_Ural_375, AirDefence.AAA_ZU_23_on_Ural_375, @@ -208,7 +206,6 @@ Units from AirDefense category of UNIT_BY_TASK that will be removed from use if """ SAM_BAN = [ AirDefence.SAM_Avenger_M1097, - AirDefence.SAM_Patriot_ICC, AirDefence.SAM_SA_19_Tunguska_2S6, AirDefence.SAM_SA_8_Osa_9A33, @@ -233,7 +230,7 @@ AirDefense units that will be spawned at control points not related to the curre """ EXTRA_AA = { "Russia": AirDefence.SAM_SA_9_Strela_1_9P31, - "USA": AirDefence.SAM_Patriot_EPP_III, + "USA": AirDefence.SAM_Avenger_M1097, } """ @@ -318,7 +315,6 @@ UNIT_BY_COUNTRY = { AirDefence.AAA_Vulcan_M163, AirDefence.SAM_Avenger_M1097, - AirDefence.SAM_Patriot_ICC, CVN_74_John_C__Stennis, LHA_1_Tarawa, diff --git a/game/event/__init__.py b/game/event/__init__.py index 3ee97e52..f9a147a6 100644 --- a/game/event/__init__.py +++ b/game/event/__init__.py @@ -4,6 +4,6 @@ from .frontlinepatrol import * from .intercept import * from .baseattack import * from .navalintercept import * -from .antiaastrike import * from .insurgentattack import * from .infantrytransport import * +from .strike import * diff --git a/game/event/antiaastrike.py b/game/event/antiaastrike.py deleted file mode 100644 index 66526adc..00000000 --- a/game/event/antiaastrike.py +++ /dev/null @@ -1,86 +0,0 @@ -import math -import random - -from dcs.task import * - -from game import * -from game.event import * -from game.operation.antiaastrike import AntiAAStrikeOperation -from userdata.debriefing import Debriefing - - -class AntiAAStrikeEvent(Event): - TARGET_AMOUNT_MAX = 2 - STRENGTH_INFLUENCE = 0.3 - SUCCESS_TARGETS_HIT_PERCENTAGE = 0.5 - - targets = None # type: db.ArmorDict - - def __str__(self): - return "Anti-AA strike" - - def is_successfull(self, debriefing: Debriefing): - total_targets = sum(self.targets.values()) - destroyed_targets = 0 - for unit, count in debriefing.destroyed_units[self.defender_name].items(): - if unit in self.targets: - destroyed_targets += count - - if self.from_cp.captured: - return math.ceil(float(destroyed_targets) / total_targets) >= self.SUCCESS_TARGETS_HIT_PERCENTAGE - else: - return math.ceil(float(destroyed_targets) / total_targets) < self.SUCCESS_TARGETS_HIT_PERCENTAGE - - def commit(self, debriefing: Debriefing): - super(AntiAAStrikeEvent, self).commit(debriefing) - - if self.from_cp.captured: - if self.is_successfull(debriefing): - self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) - else: - self.to_cp.base.affect_strength(+self.STRENGTH_INFLUENCE) - else: - if self.is_successfull(debriefing): - self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) - else: - self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) - - def skip(self): - if self.to_cp.captured: - self.to_cp.base.affect_strength(-0.1) - - def player_attacking(self, strikegroup: db.PlaneDict, clients: db.PlaneDict): - self.targets = self.to_cp.base.assemble_aa(count=self.to_cp.base.total_aa) - - op = AntiAAStrikeOperation(game=self.game, - attacker_name=self.attacker_name, - defender_name=self.defender_name, - attacker_clients=clients, - defender_clients={}, - from_cp=self.from_cp, - to_cp=self.to_cp) - op.setup(target=self.targets, - strikegroup=strikegroup, - interceptors={}) - - self.operation = op - - def player_defending(self, interceptors: db.PlaneDict, clients: db.PlaneDict): - self.targets = self.to_cp.base.assemble_aa() - - op = AntiAAStrikeOperation( - self.game, - attacker_name=self.attacker_name, - defender_name=self.defender_name, - attacker_clients={}, - defender_clients=clients, - from_cp=self.from_cp, - to_cp=self.to_cp - ) - - strikegroup = self.from_cp.base.scramble_cas(self.game.settings.multiplier) - op.setup(target=self.targets, - strikegroup=strikegroup, - interceptors=interceptors) - - self.operation = op diff --git a/game/event/event.py b/game/event/event.py index a2fc3f3a..374d5903 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -1,3 +1,5 @@ +import logging + from dcs.unittype import UnitType from game import * @@ -71,8 +73,24 @@ class Event: else: cp = self.to_cp + logging.info("base {} commit losses {}".format(cp.base, losses)) cp.base.commit_losses(losses) + for object_identifier in debriefing.destroyed_objects: + for cp in self.game.theater.controlpoints: + remove_ids = [] + if not cp.ground_objects: + continue + + for i, ground_object in enumerate(cp.ground_objects): + if ground_object.matches_string_identifier(object_identifier): + logging.info("cp {} removing ground object {}".format(cp, ground_object.string_identifier)) + remove_ids.append(i) + + remove_ids.reverse() + for i in remove_ids: + del cp.ground_objects[i] + def skip(self): pass diff --git a/game/event/intercept.py b/game/event/intercept.py index 2449bfe9..b3d63da1 100644 --- a/game/event/intercept.py +++ b/game/event/intercept.py @@ -20,7 +20,7 @@ class InterceptEvent(Event): transport_unit = None # type: FlyingType def __str__(self): - return "Intercept" + return "Air Intercept" def _enemy_scramble_multiplier(self) -> float: is_global = self.from_cp.is_global or self.to_cp.is_global diff --git a/game/event/strike.py b/game/event/strike.py new file mode 100644 index 00000000..2767b6e3 --- /dev/null +++ b/game/event/strike.py @@ -0,0 +1,46 @@ +import math +import random + +from dcs.task import * +from dcs.vehicles import * + +from game import db +from game.operation.strike import StrikeOperation +from theater.conflicttheater import * +from userdata.debriefing import Debriefing + +from .event import Event + + +class StrikeEvent(Event): + STRENGTH_INFLUENCE = 0.0 + SINGLE_OBJECT_STRENGTH_INFLUENCE = 0.03 + + def __str__(self): + return "Strike" + + def is_successfull(self, debriefing: Debriefing): + return True + + def commit(self, debriefing: Debriefing): + super(StrikeEvent, self).commit(debriefing) + self.to_cp.base.affect_strength(-self.SINGLE_OBJECT_STRENGTH_INFLUENCE * len(debriefing.destroyed_objects)) + + def player_attacking(self, strikegroup: db.PlaneDict, escort: db.PlaneDict, clients: db.PlaneDict): + op = StrikeOperation( + self.game, + attacker_name=self.attacker_name, + defender_name=self.defender_name, + attacker_clients=clients, + defender_clients={}, + from_cp=self.from_cp, + to_cp=self.to_cp + ) + + interceptors = self.to_cp.base.scramble_interceptors(self.game.settings.multiplier) + + op.setup(strikegroup=strikegroup, + escort=escort, + interceptors=interceptors) + + self.operation = op diff --git a/game/game.py b/game/game.py index 3738d263..7ee777b5 100644 --- a/game/game.py +++ b/game/game.py @@ -52,10 +52,10 @@ EVENT_PROBABILITIES = { BaseAttackEvent: [100, 10], FrontlineAttackEvent: [100, 0], FrontlinePatrolEvent: [100, 0], + StrikeEvent: [100, 0], InterceptEvent: [25, 10], InsurgentAttackEvent: [0, 10], NavalInterceptEvent: [25, 10], - AntiAAStrikeEvent: [25, 10], InfantryTransportEvent: [25, 0], } diff --git a/game/operation/antiaastrike.py b/game/operation/antiaastrike.py deleted file mode 100644 index 424705a6..00000000 --- a/game/operation/antiaastrike.py +++ /dev/null @@ -1,53 +0,0 @@ -from dcs.terrain import Terrain - -from game import db -from gen.armor import * -from gen.aircraft import * -from gen.aaa import * -from gen.shipgen import * -from gen.triggergen import * -from gen.airsupportgen import * -from gen.visualgen import * -from gen.conflictgen import Conflict - -from .operation import Operation - - -class AntiAAStrikeOperation(Operation): - strikegroup = None # type: db.PlaneDict - interceptors = None # type: db.PlaneDict - target = None # type: db.ArmorDict - - def setup(self, - target: db.ArmorDict, - strikegroup: db.PlaneDict, - interceptors: db.PlaneDict): - self.strikegroup = strikegroup - self.interceptors = interceptors - self.target = target - - def prepare(self, terrain: Terrain, is_quick: bool): - super(AntiAAStrikeOperation, self).prepare(terrain, is_quick) - if self.defender_name == self.game.player: - self.attackers_starting_position = None - self.defenders_starting_position = None - - conflict = Conflict.ground_base_attack( - attacker=self.mission.country(self.attacker_name), - defender=self.mission.country(self.defender_name), - from_cp=self.from_cp, - to_cp=self.to_cp, - theater=self.game.theater - ) - - self.initialize(mission=self.mission, - conflict=conflict) - - def generate(self): - self.airgen.generate_cas_strikegroup(self.strikegroup, clients=self.attacker_clients, at=self.attackers_starting_position) - - if self.interceptors: - self.airgen.generate_defense(self.interceptors, clients=self.defender_clients, at=self.defenders_starting_position) - - self.armorgen.generate({}, self.target) - super(AntiAAStrikeOperation, self).generate() diff --git a/game/operation/baseattack.py b/game/operation/baseattack.py index 8add9744..58bc195e 100644 --- a/game/operation/baseattack.py +++ b/game/operation/baseattack.py @@ -63,5 +63,8 @@ class BaseAttackOperation(Operation): self.airgen.generate_attackers_escort(self.escort, clients=self.attacker_clients, at=self.attackers_starting_position) self.visualgen.generate_target_smokes(self.to_cp) + + self.briefinggen.title = "Base attack" + self.briefinggen.description = "The goal of an attacker is to lower defender presence by destroying their armor and aircraft. Base will be considered captured if attackers on the ground overrun the defenders. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu." super(BaseAttackOperation, self).generate() diff --git a/game/operation/frontlineattack.py b/game/operation/frontlineattack.py index 17513925..cd43f951 100644 --- a/game/operation/frontlineattack.py +++ b/game/operation/frontlineattack.py @@ -51,4 +51,7 @@ class FrontlineAttackOperation(Operation): def generate(self): self.armorgen.generate_vec(self.attackers, self.target) self.airgen.generate_cas_strikegroup(self.strikegroup, clients=self.attacker_clients, at=self.attackers_starting_position) + + self.briefinggen.title = "Frontline CAS" + self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu." super(FrontlineAttackOperation, self).generate() diff --git a/game/operation/frontlinepatrol.py b/game/operation/frontlinepatrol.py index f4d7a355..2aa9c38f 100644 --- a/game/operation/frontlinepatrol.py +++ b/game/operation/frontlinepatrol.py @@ -52,7 +52,10 @@ class FrontlinePatrolOperation(Operation): def generate(self): self.airgen.generate_defenders_cas(self.cas, {}, self.defenders_starting_position) self.airgen.generate_defenders_escort(self.escort, {}, self.defenders_starting_position) - self.airgen.generate_patrol(self.interceptors, self.attacker_clients, self.attackers_starting_position) + self.airgen.generate_migcap(self.interceptors, self.attacker_clients, self.attackers_starting_position) self.armorgen.generate_vec(self.armor_attackers, self.armor_defenders) + + self.briefinggen.title = "Frontline CAP" + self.briefinggen.description = "Providing CAP support for ground units attacking enemy lines. Enemy will scramble its CAS and your task is to intercept it. Operation will be considered successful if total number of friendly units will be lower than enemy by at least a factor of 0.8 (i.e. with 12 units from both sides, there should be at least 8 friendly units alive), lowering targets strength as a result." super(FrontlinePatrolOperation, self).generate() diff --git a/game/operation/infantrytransport.py b/game/operation/infantrytransport.py index d327eb3a..32d8da0d 100644 --- a/game/operation/infantrytransport.py +++ b/game/operation/infantrytransport.py @@ -48,6 +48,9 @@ class InfantryTransportOperation(Operation): self.visualgen.generate_transportation_marker(self.conflict.ground_attackers_location) self.visualgen.generate_transportation_destination(self.conflict.position) + self.briefinggen.title = "Infantry transport" + self.briefinggen.description = "Helicopter operation to transport infantry troops from the base to the front line. Lowers target strength" + # TODO: horrible, horrible hack # this will disable vehicle activation triggers, # which aren't needed on this type of missions diff --git a/game/operation/insurgentattack.py b/game/operation/insurgentattack.py index e0358c68..c528949a 100644 --- a/game/operation/insurgentattack.py +++ b/game/operation/insurgentattack.py @@ -41,4 +41,7 @@ class InsurgentAttackOperation(Operation): self.airgen.generate_defense(self.strikegroup, self.defender_clients, self.defenders_starting_position) self.armorgen.generate(self.target, {}) + self.briefinggen.title = "Destroy insurgents" + self.briefinggen.description = "Destroy vehicles of insurgents in close proximity of the friendly base. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu." + super(InsurgentAttackOperation, self).generate() diff --git a/game/operation/intercept.py b/game/operation/intercept.py index 3121c794..2985a733 100644 --- a/game/operation/intercept.py +++ b/game/operation/intercept.py @@ -55,5 +55,9 @@ class InterceptOperation(Operation): self.airgen.generate_defenders_escort(self.escort, clients=self.defender_clients) self.airgen.generate_interception(self.interceptors, clients=self.attacker_clients, at=self.attackers_starting_position) + + self.briefinggen.title = "Air Intercept" + self.briefinggen.description = "Intercept enemy supply transport aircraft. Escort will also be present if there are available planes on the base. Operation will be considered successful if most of the targets are destroyed, lowering targets strength as a result" + super(InterceptOperation, self).generate() diff --git a/game/operation/navalintercept.py b/game/operation/navalintercept.py index 3dac0cf4..aa710a58 100644 --- a/game/operation/navalintercept.py +++ b/game/operation/navalintercept.py @@ -34,8 +34,6 @@ class NavalInterceptionOperation(Operation): self.initialize(self.mission, conflict) def generate(self): - super(NavalInterceptionOperation, self).generate() - target_groups = self.shipgen.generate_cargo(units=self.targets) self.airgen.generate_ship_strikegroup( @@ -51,3 +49,9 @@ class NavalInterceptionOperation(Operation): clients=self.defender_clients, at=self.defenders_starting_position ) + + self.briefinggen.title = "Naval Intercept" + self.briefinggen.description = "Destroy supply transport ships. Lowers target strength. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu." + + super(NavalInterceptionOperation, self).generate() + diff --git a/game/operation/operation.py b/game/operation/operation.py index a542ad0e..1fd70910 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -18,9 +18,11 @@ class Operation: extra_aagen = None # type: ExtraAAConflictGenerator shipgen = None # type: ShipGenerator triggersgen = None # type: TriggersGenerator - awacsgen = None # type: AirSupportConflictGenerator + airsupportgen = None # type: AirSupportConflictGenerator visualgen = None # type: VisualGenerator envgen = None # type: EnvironmentGenerator + groundobjectgen = None # type: GroundObjectsGenerator + briefinggen = None # type: BriefingGenerator environment_settings = None trigger_radius = TRIGGER_RADIUS_MEDIUM @@ -44,6 +46,12 @@ class Operation: self.to_cp = to_cp self.is_quick = False + def units_of(self, country_name: str) -> typing.Collection[UnitType]: + return [] + + def is_successfull(self, debriefing: Debriefing) -> bool: + return True + def initialize(self, mission: Mission, conflict: Conflict): self.mission = mission self.conflict = conflict @@ -52,10 +60,12 @@ class Operation: self.airgen = AircraftConflictGenerator(mission, conflict, self.game.settings) self.aagen = AAConflictGenerator(mission, conflict) self.shipgen = ShipGenerator(mission, conflict) - self.awacsgen = AirSupportConflictGenerator(mission, conflict, self.game) + self.airsupportgen = AirSupportConflictGenerator(mission, conflict, self.game) self.triggersgen = TriggersGenerator(mission, conflict, self.game) self.visualgen = VisualGenerator(mission, conflict, self.game) self.envgen = EnviromentGenerator(mission, conflict, self.game) + self.groundobjectgen = GroundObjectsGenerator(mission, conflict, self.game) + self.briefinggen = BriefingGenerator(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 @@ -78,10 +88,18 @@ class Operation: def generate(self): self.visualgen.generate() - self.awacsgen.generate(self.is_awacs_enabled) + # air support + self.airsupportgen.generate(self.is_awacs_enabled) + self.briefinggen.append_frequency("Tanker", "10X/240 MHz FM") + if self.is_awacs_enabled: + self.briefinggen.append_frequency("AWACS", "244 MHz FM") + + # ground infrastructure + self.groundobjectgen.generate() self.extra_aagen.generate() + # triggers if self.game.is_player_attack(self.conflict.attackers_side): cp = self.conflict.from_cp else: @@ -92,13 +110,16 @@ class Operation: activation_trigger_radius=self.trigger_radius, awacs_enabled=self.is_awacs_enabled) + # env settings if self.environment_settings is None: self.environment_settings = self.envgen.generate() else: self.envgen.load(self.environment_settings) - def units_of(self, country_name: str) -> typing.Collection[UnitType]: - return [] + # main frequencies + self.briefinggen.append_frequency("Flight", "251 MHz FM") + if self.conflict.from_cp.is_global or self.conflict.to_cp.is_global: + self.briefinggen.append_frequency("Carrier", "20X/ICLS CHAN1") - def is_successfull(self, debriefing: Debriefing) -> bool: - return True + # briefing + self.briefinggen.generate() diff --git a/game/operation/strike.py b/game/operation/strike.py new file mode 100644 index 00000000..85bac18a --- /dev/null +++ b/game/operation/strike.py @@ -0,0 +1,71 @@ +from dcs.terrain import Terrain + +from game import db +from gen.armor import * +from gen.aircraft import * +from gen.aaa import * +from gen.shipgen import * +from gen.triggergen import * +from gen.airsupportgen import * +from gen.visualgen import * +from gen.conflictgen import Conflict + +from .operation import Operation + + +class StrikeOperation(Operation): + strikegroup = None # type: db.PlaneDict + escort = None # type: db.PlaneDict + interceptors = None # type: db.PlaneDict + + def setup(self, + strikegroup: db.PlaneDict, + escort: db.PlaneDict, + interceptors: db.PlaneDict): + self.strikegroup = strikegroup + self.escort = escort + self.interceptors = interceptors + + def prepare(self, terrain: Terrain, is_quick: bool): + super(StrikeOperation, self).prepare(terrain, is_quick) + + self.defenders_starting_position = None + if self.game.player == self.defender_name: + self.attackers_starting_position = None + + conflict = Conflict.capture_conflict( + attacker=self.mission.country(self.attacker_name), + defender=self.mission.country(self.defender_name), + from_cp=self.from_cp, + to_cp=self.to_cp, + theater=self.game.theater + ) + + self.initialize(mission=self.mission, + conflict=conflict) + + def generate(self): + targets = [] # type: typing.List[typing.Tuple[str, Point]] + category_counters = {} # type: typing.Dict[str, int] + for object in self.to_cp.ground_objects: + category_counters[object.category] = category_counters.get(object.category, 0) + 1 + markpoint_name = "{}{}".format(object.name_abbrev, category_counters[object.category]) + targets.append((markpoint_name, object.position)) + self.briefinggen.append_target(str(object), markpoint_name) + + targets.sort(key=lambda x: self.from_cp.position.distance_to_point(x[1])) + + self.airgen.generate_ground_attack_strikegroup(strikegroup=self.strikegroup, + targets=targets, + clients=self.attacker_clients, + at=self.attackers_starting_position) + + self.airgen.generate_attackers_escort(self.escort, + clients=self.attacker_clients, + at=self.attackers_starting_position) + + self.airgen.generate_barcap(patrol=self.interceptors, + clients={}, + at=self.defenders_starting_position) + + super(StrikeOperation, self).generate() diff --git a/game/settings.py b/game/settings.py index 2e29ffba..ad4fb239 100644 --- a/game/settings.py +++ b/game/settings.py @@ -7,3 +7,4 @@ class Settings: multiplier = 1 sams = True cold_start = False + version = None diff --git a/gen/__init__.py b/gen/__init__.py index 7a5565ac..bbbf3bd0 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -7,6 +7,8 @@ from .shipgen import * from .visualgen import * from .triggergen import * from .environmentgen import * +from .groundobjectsgen import * +from .briefinggen import * from . import naming diff --git a/gen/aaa.py b/gen/aaa.py index 0fbc88f9..d9a46cc1 100644 --- a/gen/aaa.py +++ b/gen/aaa.py @@ -1,6 +1,3 @@ -from game import * - -from theater.conflicttheater import ConflictTheater from .conflictgen import * from .naming import * @@ -61,6 +58,7 @@ class ExtraAAConflictGenerator: if cp.position.distance_to_point(self.conflict.from_cp.position) < EXTRA_AA_MIN_DISTANCE: continue + print("generated extra aa for {}".format(cp)) country_name = cp.captured and self.player_name or self.enemy_name position = cp.position.point_from_heading(0, EXTRA_AA_POSITION_FROM_CP) @@ -69,6 +67,6 @@ class ExtraAAConflictGenerator: name=namegen.next_basedefense_name(), _type=db.EXTRA_AA[country_name], position=position, - group_size=2 + group_size=1 ) diff --git a/gen/aircraft.py b/gen/aircraft.py index 8ad923b6..12fa911d 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -265,6 +265,30 @@ class AircraftConflictGenerator: self.escort_targets.append((group, group.points.index(waypoint))) self._rtb_for(group, self.conflict.from_cp, at) + def generate_ground_attack_strikegroup(self, strikegroup: db.PlaneDict, targets: typing.List[typing.Tuple[str, Point]], clients: db.PlaneDict, at: db.StartingPosition = None): + assert len(self.escort_targets) == 0 + + for flying_type, count, client_count in self._split_to_groups(strikegroup, clients): + group = self._generate_group( + name=namegen.next_unit_name(self.conflict.attackers_side, flying_type), + side=self.conflict.attackers_side, + unit_type=flying_type, + count=count, + client_count=client_count, + at=at and at or self._group_point(self.conflict.air_attackers_location)) + + escort_until_waypoint = None + + for name, pos in targets: + waypoint = group.add_waypoint(pos, WARM_START_ALTITUDE, WARM_START_AIRSPEED, self.m.translation.create_string(name)) + if escort_until_waypoint is None: + escort_until_waypoint = waypoint + + group.task = CAS.name + self._setup_group(group, CAS, client_count) + self.escort_targets.append((group, group.points.index(escort_until_waypoint))) + self._rtb_for(group, self.conflict.from_cp, at) + def generate_defenders_cas(self, defenders: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None): assert len(self.escort_targets) == 0 @@ -348,7 +372,7 @@ class AircraftConflictGenerator: self._setup_group(group, CAP, client_count) self._rtb_for(group, self.conflict.to_cp, at) - def generate_patrol(self, patrol: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None): + def generate_migcap(self, patrol: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None): for flying_type, count, client_count in self._split_to_groups(patrol, clients): group = self._generate_group( name=namegen.next_unit_name(self.conflict.attackers_side, flying_type), @@ -366,6 +390,26 @@ class AircraftConflictGenerator: self._setup_group(group, CAP, client_count) self._rtb_for(group, self.conflict.from_cp, at) + def generate_barcap(self, patrol: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None): + for flying_type, count, client_count in self._split_to_groups(patrol, clients): + group = self._generate_group( + name=namegen.next_unit_name(self.conflict.defenders_side, flying_type), + side=self.conflict.defenders_side, + unit_type=flying_type, + count=count, + client_count=client_count, + at=at and at or self._group_point(self.conflict.air_defenders_location)) + + waypoint = group.add_waypoint(self.conflict.position, WARM_START_ALTITUDE, WARM_START_AIRSPEED) + if self.conflict.is_vector: + group.add_waypoint(self.conflict.tail, WARM_START_ALTITUDE, WARM_START_AIRSPEED) + else: + waypoint.tasks.append(OrbitAction(WARM_START_ALTITUDE, WARM_START_AIRSPEED)) + + group.task = CAP.name + self._setup_group(group, CAP, client_count) + self._rtb_for(group, self.conflict.to_cp, at) + def generate_transport(self, transport: db.PlaneDict, destination: Airport): assert len(self.escort_targets) == 0 diff --git a/gen/briefinggen.py b/gen/briefinggen.py new file mode 100644 index 00000000..851fe3fd --- /dev/null +++ b/gen/briefinggen.py @@ -0,0 +1,49 @@ +import logging + +from game import db +from .conflictgen import * +from .naming import * + +from dcs.mission import * + + +class BriefingGenerator: + freqs = None # type: typing.List[typing.Tuple[str, str]] + title = "" # type: str + description = "" # type: str + targets = None # type: typing.List[typing.Tuple[str, str]] + + def __init__(self, mission: Mission, conflict: Conflict, game): + self.m = mission + self.conflict = conflict + self.game = game + + self.freqs = [] + self.targets = [] + + def append_frequency(self, name: str, frequency: str): + self.freqs.append((name, frequency)) + + def append_target(self, description: str, markpoint: str = None): + self.targets.append((description, markpoint)) + + def generate(self): + description = "" + + if self.title: + description += self.title + + if self.description: + description += "\n\n" + self.description + + if self.freqs: + description += "\n\n RADIO:" + for name, freq in self.freqs: + description += "\n{}: {}".format(name, freq) + + if self.targets: + description += "\n\n TARGETS:" + for name, tp in self.targets: + description += "\n{} {}".format(name, "(TP {})".format(tp) if tp else "") + + self.m.set_description_text(description) diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py new file mode 100644 index 00000000..b4d9f16b --- /dev/null +++ b/gen/groundobjectsgen.py @@ -0,0 +1,57 @@ +import logging + +from game import db +from .conflictgen import * +from .naming import * + +from dcs.mission import * +from dcs.statics import * + + +CATEGORY_MAPPING = { + "power": [Fortification.Workshop_A], + "warehouse": [Warehouse.Warehouse], + "fuel": [Warehouse.Tank], + "ammo": [Warehouse.Ammunition_depot], +} + + +class GroundObjectsGenerator: + def __init__(self, mission: Mission, conflict: Conflict, game): + self.m = mission + self.conflict = conflict + self.game = game + + def generate(self): + side = self.m.country(self.game.enemy) + + cp = None # type: ControlPoint + if self.conflict.attackers_side.name == self.game.player: + cp = self.conflict.to_cp + else: + cp = self.conflict.from_cp + + for ground_object in cp.ground_objects: + if ground_object.category == "defense": + unit_type = random.choice(self.game.commision_unit_types(cp, AirDefence)) + assert unit_type is not None, "Cannot find unit type for GroundObject defense ({})!".format(cp) + + group = self.m.vehicle_group( + country=side, + name=ground_object.string_identifier, + _type=unit_type, + position=Point(*ground_object.location), + heading=ground_object.heading + ) + + logging.info("generated defense object identifier {} with mission id {}".format(group.name, group.id)) + else: + group = self.m.static_group( + country=side, + name=ground_object.string_identifier, + _type=random.choice(CATEGORY_MAPPING[ground_object.category]), + position=Point(*ground_object.location), + heading=ground_object.heading + ) + + logging.info("generated object identifier {} with mission id {}".format(group.name, group.id)) diff --git a/gen/triggergen.py b/gen/triggergen.py index f199f841..f797eff4 100644 --- a/gen/triggergen.py +++ b/gen/triggergen.py @@ -146,19 +146,6 @@ class TriggersGenerator: self._set_skill(player_coalition, enemy_coalition) self._set_allegiances(player_coalition, enemy_coalition) - description = "" - description += "FREQUENCIES:" - description += "\nFlight: 251 MHz AM" - description += "\nTanker: 10X/240 MHz" - - if awacs_enabled: - description += "\nAWACS: 244 MHz" - - if self.conflict.from_cp.is_global or self.conflict.to_cp.is_global: - description += "\nCarrier: 20X/ICLS CHAN1" - - self.mission.set_description_text(description) - if not is_quick: # TODO: waypoint parts of this should not be post-hacked but added in airgen self._gen_activation_trigger(activation_trigger_radius, player_cp, player_coalition, enemy_coalition) diff --git a/resources/cau_groundobjects.p b/resources/cau_groundobjects.p new file mode 100644 index 00000000..4a47c810 Binary files /dev/null and b/resources/cau_groundobjects.p differ diff --git a/resources/tools/cau_groundobjects.miz b/resources/tools/cau_groundobjects.miz new file mode 100644 index 00000000..aefc0e55 Binary files /dev/null and b/resources/tools/cau_groundobjects.miz differ diff --git a/resources/tools/generate_groundobjectsmap.py b/resources/tools/generate_groundobjectsmap.py new file mode 100644 index 00000000..8557fb23 --- /dev/null +++ b/resources/tools/generate_groundobjectsmap.py @@ -0,0 +1,44 @@ +import pickle +import typing + +from game import db +from gen.groundobjectsgen import TheaterGroundObject +from dcs.mission import Mission +from dcs.terrain import PersianGulf + +m = Mission() +m.load_file("./cau_groundobjects.miz") + +result = {} + + +def append_group(cp_id, category, group_id, object_id, position, heading): + global result + + if cp_id not in result: + result[cp_id] = [] + + result[cp_id].append(TheaterGroundObject(category, cp_id, group_id, object_id, position, heading)) + + +def parse_name(name: str) -> typing.Tuple: + args = str(name).split("|") + if len(args) == 3: + args.append("1") + + return args[0], int(args[1]), int(args[2]), int(args[3]) + + +for group in m.country("Russia").static_group + m.country("Russia").vehicle_group: + try: + category, cp_id, group_id, object_id = parse_name(str(group.name)) + except: + print("Failed to parse {}".format(group.name)) + continue + + append_group(cp_id, category, group_id, object_id, [group.position.x, group.position.y], group.units[0].heading) + +print("Total {} objects".format(sum([len(x) for x in result.values()]))) +with open("../cau_groundobjects.p", "wb") as f: + pickle.dump(result, f) + diff --git a/resources/tools/generate_loadout_check.py b/resources/tools/generate_loadout_check.py index 88d0b919..575b8519 100644 --- a/resources/tools/generate_loadout_check.py +++ b/resources/tools/generate_loadout_check.py @@ -1,10 +1,14 @@ +import os import dcs -from gen.aircraft import AircraftConflictGenerator from game import db +from gen.aircraft import AircraftConflictGenerator + +dcs.planes.FlyingType.payload_dirs = [os.path.join(os.path.dirname(os.path.realpath(__file__)), "..\\payloads")] + mis = dcs.Mission(dcs.terrain.PersianGulf()) pos = dcs.terrain.PersianGulf().khasab().position -airgen = AircraftConflictGenerator(mis, None) +airgen = AircraftConflictGenerator(mis, None, None) for t, uts in db.UNIT_BY_TASK.items(): if t != dcs.task.CAP and t != dcs.task.CAS: @@ -25,6 +29,6 @@ for t, uts in db.UNIT_BY_TASK.items(): altitude=10000 ) g.task = t.name - airgen._setup_group(g, t) + airgen._setup_group(g, t, 0) mis.save("loadout_test.miz") diff --git a/resources/tools/mkrelease.py b/resources/tools/mkrelease.py index 09bf352a..c8776e78 100644 --- a/resources/tools/mkrelease.py +++ b/resources/tools/mkrelease.py @@ -16,7 +16,7 @@ IGNORED_PATHS = [ "venv", ] -VERSION = "1.3.3" +VERSION = input("version str:") def _zip_dir(archieve, path): diff --git a/theater/caucasus.py b/theater/caucasus.py index 7d075f13..82e1c4b6 100644 --- a/theater/caucasus.py +++ b/theater/caucasus.py @@ -73,7 +73,10 @@ class CaucasusTheater(ConflictTheater): self.carrier_1.captured = True self.soganlug.captured = True + with open("resources/cau_groundobjects.p", "rb") as f: + self.set_groundobject(pickle.load(f)) + def add_controlpoint(self, point: ControlPoint, connected_to: typing.Collection[ControlPoint] = []): - point.name = " ".join(re.split(r" |-", point.name)[:1]) + point.name = " ".join(re.split(r"[ -]", point.name)[:1]) super(CaucasusTheater, self).add_controlpoint(point, connected_to=connected_to) \ No newline at end of file diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index ef65b03c..baa3f445 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -6,6 +6,7 @@ from dcs.mapping import Point from .landmap import ray_tracing from .controlpoint import ControlPoint +from .theatergroundobject import TheaterGroundObject SIZE_TINY = 150 SIZE_SMALL = 600 @@ -48,6 +49,7 @@ COAST_DR_W = [135, 180, 225, 315] class ConflictTheater: terrain = None # type: dcs.terrain.Terrain controlpoints = None # type: typing.Collection[ControlPoint] + reference_points = None # type: typing.Dict overview_image = None # type: str landmap_poly = None @@ -55,6 +57,14 @@ class ConflictTheater: def __init__(self): self.controlpoints = [] + self.groundobjects = [] + + def set_groundobject(self, dictionary: typing.Dict[int, typing.Collection[TheaterGroundObject]]): + for id, value in dictionary.items(): + for cp in self.controlpoints: + if cp.id == id: + cp.ground_objects = value + break def add_controlpoint(self, point: ControlPoint, connected_to: typing.Collection[ControlPoint] = []): for connected_point in connected_to: diff --git a/theater/controlpoint.py b/theater/controlpoint.py index dff1deab..f8985244 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -5,18 +5,23 @@ from dcs.mapping import * from dcs.country import * from dcs.terrain import Airport +from .theatergroundobject import TheaterGroundObject + class ControlPoint: connected_points = [] # type: typing.List[ControlPoint] + ground_objects = None # type: typing.Collection[TheaterGroundObject] position = None # type: Point captured = False has_frontline = True + id = 0 base = None # type: theater.base.Base at = None # type: db.StartPosition - def __init__(self, name: str, position: Point, at, radials: typing.Collection[int], size: int, importance: int, has_frontline=True): + def __init__(self, id: int, name: str, position: Point, at, radials: typing.Collection[int], size: int, importance: int, has_frontline=True): import theater.base + self.id = id self.name = " ".join(re.split(r" |-", name)[:2]) self.full_name = name self.position = position @@ -33,12 +38,12 @@ class ControlPoint: @classmethod def from_airport(cls, airport: Airport, radials: typing.Collection[int], size: int, importance: int, has_frontline=True): assert airport - return cls(airport.name, airport.position, airport, radials, size, importance, has_frontline) + return cls(airport.id, airport.name, airport.position, airport, radials, size, importance, has_frontline) @classmethod def carrier(cls, name: str, at: Point): import theater.conflicttheater - return cls(name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1) + return cls(0, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1) def __str__(self): return self.name diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py new file mode 100644 index 00000000..b0e3a3fa --- /dev/null +++ b/theater/theatergroundobject.py @@ -0,0 +1,54 @@ +import typing + +from dcs.mapping import Point + +NAME_BY_CATEGORY = { + "power": "Power plant", + "ammo": "Ammo depot", + "fuel": "Fuel depot", + "defense": "AA Defense Site", + "warehouse": "Warehouse", +} + +ABBREV_NAME = { + "power": "PLANT", + "ammo": "AMMO", + "fuel": "FUEL", + "defense": "AA", + "warehouse": "WARE", +} + + +class TheaterGroundObject: + object_id = 0 + cp_id = 0 + group_id = 0 + heading = 0 + location = None # type: typing.Collection[int] + category = None # type: str + + def __init__(self, category, cp_id, group_id, object_id, location, heading): + self.category = category + self.cp_id = cp_id + self.group_id = group_id + self.object_id = object_id + self.location = location + self.heading = heading + + @property + def string_identifier(self): + return "{}|{}|{}|{}".format(self.category, self.cp_id, self.group_id, self.object_id) + + @property + def position(self) -> Point: + return Point(*self.location) + + @property + def name_abbrev(self) -> str: + return ABBREV_NAME[self.category] + + def __str__(self): + return NAME_BY_CATEGORY[self.category] + + def matches_string_identifier(self, id): + return self.string_identifier == id diff --git a/ui/eventmenu.py b/ui/eventmenu.py index 26ae3f3d..a7673038 100644 --- a/ui/eventmenu.py +++ b/ui/eventmenu.py @@ -4,25 +4,31 @@ from ui.eventresultsmenu import * from game import * from game.event import * -from .styles import STYLES +from .styles import STYLES, RED UNITTYPES_FOR_EVENTS = { FrontlineAttackEvent: [CAS, PinpointStrike], FrontlinePatrolEvent: [CAP, PinpointStrike], BaseAttackEvent: [CAP, CAS, PinpointStrike], + StrikeEvent: [CAP, CAS], InterceptEvent: [CAP], InsurgentAttackEvent: [CAS], NavalInterceptEvent: [CAS], - AntiAAStrikeEvent: [CAS], InfantryTransportEvent: [Embarking], } +AI_BAN_FOR_EVENTS = { + InfantryTransportEvent: [Embarking], + StrikeEvent: [CAS], +} + class EventMenu(Menu): aircraft_scramble_entries = None # type: typing.Dict[PlaneType , Entry] aircraft_client_entries = None # type: typing.Dict[PlaneType, Entry] armor_scramble_entries = None # type: typing.Dict[VehicleType, Entry] + error_label = None # type: Label awacs = None # type: IntVar def __init__(self, window: Window, parent, game: Game, event: event.Event): @@ -54,11 +60,14 @@ class EventMenu(Menu): def label(text, _row=None, _column=None, sticky=None): nonlocal row - Label(self.frame, text=text, **STYLES["widget"]).grid(row=_row and _row or row, column=_column and _column or 0, sticky=sticky) + new_label = Label(self.frame, text=text, **STYLES["widget"]) + new_label.grid(row=_row and _row or row, column=_column and _column or 0, sticky=sticky) if _row is None: row += 1 + return new_label + def scrable_row(unit_type, unit_count): nonlocal row Label(self.frame, text="{} ({})".format(db.unit_type_name(unit_type), unit_count), **STYLES["widget"]).grid(row=row, sticky=W) @@ -138,6 +147,8 @@ class EventMenu(Menu): row += 1 header("Ready ?") + self.error_label = label("") + self.error_label["fg"] = RED Button(self.frame, text="Commit", command=self.start, **STYLES["btn-primary"]).grid(column=0, row=row, sticky=E, padx=5, pady=(10,10)) Button(self.frame, text="Back", command=self.dismiss, **STYLES["btn-warning"]).grid(column=3, row=row, sticky=E, padx=5, pady=(10,10)) row += 1 @@ -214,6 +225,16 @@ class EventMenu(Menu): if amount > 0: scrambled_armor[unit_type] = amount + if type(self.event) in AI_BAN_FOR_EVENTS: + banned_tasks_for_ai = AI_BAN_FOR_EVENTS[type(self.event)] + for task in banned_tasks_for_ai: + scrambled_slots = [1 for x in scrambled_aircraft if db.unit_task(x) == task] + scrambled_client_slots = [1 for x in scrambled_clients if db.unit_task(x) == task] + + if scrambled_slots != scrambled_client_slots: + self.error_label["text"] = "AI slots of task {} are not supported for this operation".format(task.name) + return + if type(self.event) is BaseAttackEvent: e = self.event # type: BaseAttackEvent if self.game.is_player_attack(self.event): @@ -263,6 +284,14 @@ class EventMenu(Menu): e.player_attacking(transport=scrambled_aircraft, clients=scrambled_clients) else: assert False + elif type(self.event) is StrikeEvent: + e = self.event # type: StrikeEvent + if self.game.is_player_attack(self.event): + e.player_attacking(strikegroup=scrambled_cas, + escort=scrambled_sweep, + clients=scrambled_clients) + else: + assert False self.game.initiate_event(self.event) EventResultsMenu(self.window, self.parent, self.game, self.event).display() diff --git a/ui/eventresultsmenu.py b/ui/eventresultsmenu.py index 16db01d2..dc149bed 100644 --- a/ui/eventresultsmenu.py +++ b/ui/eventresultsmenu.py @@ -54,12 +54,9 @@ class EventResultsMenu(Menu): pg.start(10) row += 1 - Separator(self.frame, orient=HORIZONTAL).grid(row=row, sticky=EW); - row += 1 - Label(self.frame, text="Cheat operation results: ", **STYLES["strong"]).grid(column=0, row=row, columnspan=2, sticky=NSEW, - pady=5); + pady=5) row += 1 Button(self.frame, text="full enemy losses", command=self.simulate_result(0, 1), @@ -116,7 +113,7 @@ class EventResultsMenu(Menu): def simulate_result(self, player_factor: float, enemy_factor: float): def action(): - debriefing = Debriefing({}) + debriefing = Debriefing({}, []) def count(country: Country) -> typing.Dict[UnitType, int]: result = {} diff --git a/userdata/debriefing.py b/userdata/debriefing.py index 9b641578..4cce6290 100644 --- a/userdata/debriefing.py +++ b/userdata/debriefing.py @@ -59,14 +59,59 @@ def parse_mutliplayer_debriefing(contents: str): class Debriefing: - def __init__(self, dead_units): + def __init__(self, dead_units, dead_objects): self.destroyed_units = {} # type: typing.Dict[str, typing.Dict[UnitType, int]] self.alive_units = {} # type: typing.Dict[str, typing.Dict[UnitType, int]] + self.destroyed_objects = [] # type: typing.List[str] self._dead_units = dead_units + self._dead_objects = dead_objects @classmethod def parse(cls, path: str): + dead_units = {} + dead_objects = [] + + def append_dead_unit(country_id, unit_type): + nonlocal dead_units + if country_id not in dead_units: + dead_units[country_id] = {} + + if unit_type not in dead_units[country_id]: + dead_units[country_id][unit_type] = 0 + + dead_units[country_id][unit_type] += 1 + + def append_dead_object(object_mission_id_str): + nonlocal dead_objects + object_mission_id = int(object_mission_id_str) + if object_mission_id in dead_objects: + logging.info("debriefing: failed to append_dead_object {}: already exists!".format(object_mission_id)) + return + + dead_objects.append(object_mission_id) + + def parse_dead_unit(event): + try: + components = event["initiator"].split("|") + category, country_id, group_id, unit_type = components[0], int(components[1]), int(components[2]), db.unit_type_from_name(components[3]) + if unit_type is None: + logging.info("Skipped {} due to no unit type".format(event)) + return + + if category == "unit": + append_dead_unit(country_id, unit_type) + else: + logging.info("Skipped {} due to category".format(event)) + except Exception as e: + logging.error(e) + + def parse_dead_object(event): + try: + append_dead_object(event["initiatorMissionID"]) + except Exception as e: + logging.error(e) + with open(path, "r") as f: table_string = f.read() try: @@ -75,36 +120,18 @@ class Debriefing: table = parse_mutliplayer_debriefing(table_string) events = table.get("debriefing", {}).get("events", {}) - dead_units = {} - for event in events.values(): event_type = event.get("type", None) - if event_type != "crash" and event_type != "dead": - continue + if event_type in ["crash", "dead"]: + object_initiator = event["initiator"] in ["SKLAD_CRUSH", "SKLADCDESTR", "TEC_A_CRUSH", "BAK_CRUSH"] + defense_initiator = event["initiator"].startswith("defense|") - try: - components = event["initiator"].split("|") - category, country_id, group_id, unit_type = components[0], int(components[1]), int(components[2]), db.unit_type_from_name(components[3]) - if unit_type is None: - logging.info("Skipped due to no unit type") - continue + if object_initiator or defense_initiator: + parse_dead_object(event) + else: + parse_dead_unit(event) - if category != "unit": - logging.info("Skipped due to category") - continue - except Exception as e: - logging.error(e) - continue - - if country_id not in dead_units: - dead_units[country_id] = {} - - if unit_type not in dead_units[country_id]: - dead_units[country_id][unit_type] = 0 - - dead_units[country_id][unit_type] += 1 - - return Debriefing(dead_units) + return Debriefing(dead_units, dead_objects) def calculate_units(self, mission: Mission, player_name: str, enemy_name: str): def count_groups(groups: typing.List[UnitType]) -> typing.Dict[UnitType, int]: @@ -142,6 +169,13 @@ class Debriefing: enemy.name: {k: v - self.destroyed_units[enemy.name].get(k, 0) for k, v in enemy_units.items()}, } + for mission_id in self._dead_objects: + for group in mission.country(enemy.name).static_group + mission.country(enemy.name).vehicle_group: + if group.id == mission_id: + self.destroyed_objects.append(str(group.name)) + + self.destroyed_objects += self._dead_defense + def debriefing_directory_location() -> str: return os.path.join(base_path(), "liberation_debriefings")