diff --git a/.gitignore b/.gitignore index 2aa12800..780b6059 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,14 @@ *.pyc __pycache__ -build/* +build/** resources/payloads/*.lua venv logs.txt .DS_Store +dist/** +a.py +resources/tools/a.miz +tests/** # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml diff --git a/.idea/modules.xml b/.idea/modules.xml index b52058de..c32a91ea 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,7 +3,6 @@ - \ No newline at end of file diff --git a/__init__.py b/__init__.py index e297774b..62d099e6 100755 --- a/__init__.py +++ b/__init__.py @@ -1,21 +1,16 @@ #!/usr/bin/env python3 +import logging import os import re import sys + import dcs -import logging -import theater.caucasus -import theater.persiangulf -import theater.nevada - -import ui.window +import ui.corruptedsavemenu import ui.mainmenu import ui.newgamemenu -import ui.corruptedsavemenu - +import ui.window 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" @@ -53,40 +48,11 @@ def is_version_compatible(save_version): w = ui.window.Window() + try: game = persistency.restore_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): - if terrain == "persiangulf": - conflicttheater = theater.persiangulf.PersianGulfTheater() - elif terrain == "nevada": - conflicttheater = theater.nevada.NevadaTheater() - else: - conflicttheater = theater.caucasus.CaucasusTheater() - - if midgame: - for i in range(0, int(len(conflicttheater.controlpoints) / 2)): - conflicttheater.controlpoints[i].captured = True - - start_generator.generate_inital_units(conflicttheater, enemy_name, sams, multiplier) - start_generator.generate_groundobjects(conflicttheater) - game = Game(player_name=player_name, - enemy_name=enemy_name, - theater=conflicttheater) - 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())) - - proceed_to_main_menu(game) - - new_game_menu = ui.newgamemenu.NewGameMenu(w, start_new_game) - new_game_menu.display() + ui.newgamemenu.NewGameMenu(w, w.start_new_game).display() else: game.settings.version = VERSION_STRING proceed_to_main_menu(game) diff --git a/a.py b/a.py new file mode 100644 index 00000000..6e421e47 --- /dev/null +++ b/a.py @@ -0,0 +1,38 @@ +from theater.caucasus import * +from gen.conflictgen import Conflict + +from matplotlib import pyplot +from matplotlib import lines +from shapely import geometry +from shapely.geometry import Polygon +from descartes.patch import PolygonPatch + +def put_lines(ls, ax): + for g in ls.geoms: + ax.plot([g.xy[0][0], g.xy[0][1]], [g.xy[1][0], g.xy[1][1]]) + +cau = CaucasusTheater() +#left, heading, dist = Conflict.frontline_vector(cau.soganlug, cau.kutaisi, cau) +#right = left.point_from_heading(heading, dist) + +left, heading = Conflict.frontline_position(cau, cau.soganlug, cau.kutaisi) +right = left.point_from_heading(heading+90, 80000) +left = left.point_from_heading(heading-90, 80000) + +line = geometry.LineString([(left.x, left.y), (right.x, right.y)]) +line = line.intersection(cau.land_poly) + +fig = pyplot.figure(1, figsize=(20, 20), dpi=90) +ax = fig.add_subplot(121) +ax.set_ylim([0, 1500000]) +ax.set_xlim([-600000, 400000]) + +patch = PolygonPatch(cau.land_poly, facecolor=(0, 0, 0), edgecolor=(0, 0, 0), alpha=0.5, zorder=2) +ax.add_patch(patch) +ax.plot([left.x, right.x], [left.y, right.y], 'k-', lw=2) + +ax.plot([cau.soganlug.position.x, cau.soganlug.position.x+1000], [cau.soganlug.position.y, cau.soganlug.position.y+1000], lw=5) +ax.plot([cau.kutaisi.position.x, cau.kutaisi.position.x+1000], [cau.kutaisi.position.y, cau.kutaisi.position.y+1000], lw=5) +put_lines(line, ax) +pyplot.show() + diff --git a/game/db.py b/game/db.py index f61ee379..3b155721 100644 --- a/game/db.py +++ b/game/db.py @@ -39,12 +39,11 @@ and prioritization for the enemy (i.e. less important bases will receive units w """ PRICES = { # fighter - C_101CC: 8, - MiG_23MLD: 18, - Su_27: 20, + MiG_23MLD: 13, + Su_27: 18, Su_33: 22, - MiG_29A: 23, - MiG_29S: 25, + MiG_29A: 18, + MiG_29S: 20, F_5E_3: 6, MiG_15bis: 5, @@ -55,6 +54,7 @@ PRICES = { M_2000C: 13, FA_18C_hornet: 18, F_15C: 20, + F_14B: 14, # bomber Su_25: 15, @@ -85,14 +85,14 @@ PRICES = { C_130: 8, # armor - Armor.APC_BTR_80: 12, - Armor.MBT_T_55: 14, - Armor.MBT_T_80U: 18, - Armor.MBT_T_90: 20, + Armor.APC_BTR_80: 16, + Armor.MBT_T_55: 22, + Armor.MBT_T_80U: 28, + Armor.MBT_T_90: 35, - Armor.ATGM_M1134_Stryker: 12, - Armor.MBT_M60A3_Patton: 14, - Armor.MBT_M1A2_Abrams: 18, + Armor.ATGM_M1134_Stryker: 18, + Armor.MBT_M60A3_Patton: 24, + Armor.MBT_M1A2_Abrams: 35, Unarmed.Transport_UAZ_469: 3, Unarmed.Transport_Ural_375: 3, @@ -137,7 +137,6 @@ Following tasks are present: """ UNIT_BY_TASK = { CAP: [ - C_101CC, F_5E_3, MiG_23MLD, Su_27, @@ -147,6 +146,7 @@ UNIT_BY_TASK = { MiG_29S, FA_18C_hornet, F_15C, + F_14B, M_2000C, ], CAS: [ @@ -235,7 +235,6 @@ SAM_BAN = [ Units that will always be spawned in the air """ TAKEOFF_BAN = [ - AV8BNA, # AI takeoff currently bugged attempting VTOL with no regards for the total weight ] """ @@ -259,7 +258,6 @@ Be advised that putting unit to the country that have not access to the unit in """ UNIT_BY_COUNTRY = { "Russia": [ - C_101CC, AJS37, MiG_23MLD, F_5E_3, @@ -308,6 +306,7 @@ UNIT_BY_COUNTRY = { "USA": [ F_5E_3, F_15C, + F_14B, FA_18C_hornet, AJS37, M_2000C, @@ -345,6 +344,7 @@ UNIT_BY_COUNTRY = { CARRIER_TYPE_BY_PLANE = { FA_18C_hornet: CVN_74_John_C__Stennis, + F_14B: CVN_74_John_C__Stennis, Ka_50: LHA_1_Tarawa, SA342M: LHA_1_Tarawa, UH_1H: LHA_1_Tarawa, @@ -371,16 +371,25 @@ Payload will be used for operation of following type, "*" category will be used PLANE_PAYLOAD_OVERRIDES = { FA_18C_hornet: { CAP: "AIM-120*4,AIM-9*2,AIM-7*2,Fuel", + Escort: "AIM-120*4,AIM-9*2,AIM-7*2,Fuel", PinpointStrike: "MK-82*8,AIM-9*2,AIM-7,FLIR Pod,Fuel", AntishipStrike: "MK-82*8,AIM-9*2,AIM-7,FLIR Pod,Fuel", }, + F_14B: { + CAP: "AIM-54A-MK47*4, AIM-7M*2, AIM-9M*2, XT*2", + Escort: "AIM-54A-MK47*4, AIM-7M*2, AIM-9M*2, XT*2", + CAS: "AIM-54A-MK60*1, AIM-7M*1, AIM-9M*2, XT*2, Mk-82*2, LANTIRN", + GroundAttack: "AIM54, AIM-9M*2, XT*2, GBU-12*4, LANTIRN", + }, + Su_25T: { CAS: "APU-8 Vikhr-M*2,Kh-25ML,R-73*2,SPPU-22*2,Mercury LLTV Pod,MPS-410", }, Su_33: { CAP: "R-73*4,R-27R*2,R-27ER*6", + Escort: "R-73*4,R-27R*2,R-27ER*6", }, AJS37: { @@ -403,6 +412,7 @@ PLANE_PAYLOAD_OVERRIDES = { M_2000C: { CAP: "Combat Air Patrol", + Escort: "Combat Air Patrol", GroundAttack: "MK-82S Heavy Strike", }, @@ -487,6 +497,7 @@ def task_name(task) -> str: def choose_units(for_task: Task, factor: float, count: int, country: str) -> typing.Collection[UnitType]: suitable_unittypes = find_unittype(for_task, country) + suitable_unittypes = [x for x in suitable_unittypes if x not in helicopter_map.values()] suitable_unittypes.sort(key=lambda x: PRICES[x]) idx = int(len(suitable_unittypes) * factor) 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/baseattack.py b/game/event/baseattack.py index 6a3b8601..77a7cb03 100644 --- a/game/event/baseattack.py +++ b/game/event/baseattack.py @@ -25,10 +25,10 @@ class BaseAttackEvent(Event): return "Ground attack" def is_successfull(self, debriefing: Debriefing): - alive_attackers = sum([v for k, v in debriefing.alive_units[self.attacker_name].items() if db.unit_task(k) == PinpointStrike]) - alive_defenders = sum([v for k, v in debriefing.alive_units[self.defender_name].items() if db.unit_task(k) == PinpointStrike]) + alive_attackers = sum([v for k, v in debriefing.alive_units.get(self.attacker_name, {}).items() if db.unit_task(k) == PinpointStrike]) + alive_defenders = sum([v for k, v in debriefing.alive_units.get(self.defender_name, {}).items() if db.unit_task(k) == PinpointStrike]) attackers_success = alive_attackers >= alive_defenders - if self.from_cp.captured: + if self.departure_cp.captured: return attackers_success else: return not attackers_success @@ -36,14 +36,14 @@ class BaseAttackEvent(Event): def commit(self, debriefing: Debriefing): super(BaseAttackEvent, self).commit(debriefing) if self.is_successfull(debriefing): - if self.from_cp.captured: + if self.departure_cp.captured: self.to_cp.captured = True self.to_cp.ground_objects = [] self.to_cp.base.filter_units(db.UNIT_BY_COUNTRY[self.attacker_name]) self.to_cp.base.affect_strength(+self.STRENGTH_RECOVERY) else: - if not self.from_cp.captured: + if not self.departure_cp.captured: self.to_cp.captured = False self.to_cp.base.affect_strength(+self.STRENGTH_RECOVERY) @@ -54,14 +54,15 @@ class BaseAttackEvent(Event): def player_defending(self, flights: db.TaskForceDict): assert CAP in flights and len(flights) == 1, "Invalid scrambled flights" - cas = self.from_cp.base.scramble_cas(self.game.settings.multiplier) - escort = self.from_cp.base.scramble_sweep(self.game.settings.multiplier) - attackers = self.from_cp.base.armor + cas = self.departure_cp.base.scramble_cas(self.game.settings.multiplier) + escort = self.departure_cp.base.scramble_sweep(self.game.settings.multiplier) + attackers = self.departure_cp.base.armor op = BaseAttackOperation(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(cas=assigned_units_from(cas), @@ -80,6 +81,7 @@ class BaseAttackEvent(Event): 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) defenders = self.to_cp.base.scramble_sweep(self.game.settings.multiplier) diff --git a/game/event/convoystrike.py b/game/event/convoystrike.py new file mode 100644 index 00000000..5aea7940 --- /dev/null +++ b/game/event/convoystrike.py @@ -0,0 +1,83 @@ +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.6 + STRENGTH_INFLUENCE = 0.25 + + targets = None # type: db.ArmorDict + + @property + def threat_description(self): + return "" + + @property + def tasks(self): + return [CAS] + + @property + def global_cp_available(self) -> bool: + return True + + 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.get(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/event.py b/game/event/event.py index 39692148..d0475472 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -9,12 +9,14 @@ from dcs.unittype import UnitType from game import * from theater import * from gen.environmentgen import EnvironmentSettings +from gen.conflictgen import Conflict from game.db import assigned_units_from, unitdict_from from userdata.debriefing import Debriefing from userdata import persistency DIFFICULTY_LOG_BASE = 1.1 +EVENT_DEPARTURE_MAX_DISTANCE = 340000 class Event: @@ -22,18 +24,26 @@ class Event: informational = False is_awacs_enabled = False ca_slots = 0 + + game = None # type: Game + location = None # type: Point + from_cp = None # type: ControlPoint + departure_cp = None # type: ControlPoint + to_cp = None # type: ControlPoint + operation = None # type: Operation difficulty = 1 # type: int - game = None # type: Game environment_settings = None # type: EnvironmentSettings BONUS_BASE = 5 - def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game): + def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str): + self.game = game + self.departure_cp = None + self.from_cp = from_cp + self.to_cp = target_cp + self.location = location self.attacker_name = attacker_name self.defender_name = defender_name - self.to_cp = to_cp - self.from_cp = from_cp - self.game = game @property def is_player_attacking(self) -> bool: @@ -44,7 +54,7 @@ class Event: if self.attacker_name == self.game.player: return self.to_cp else: - return self.from_cp + return self.departure_cp @property def threat_description(self) -> str: @@ -61,17 +71,43 @@ class Event: def ai_banned_tasks(self) -> typing.Collection[typing.Type[Task]]: return [] + @property + def player_banned_tasks(self) -> typing.Collection[typing.Type[Task]]: + return [] + + @property + def global_cp_available(self) -> bool: + return False + + def is_departure_available_from(self, cp: ControlPoint) -> bool: + if not cp.captured: + return False + + if self.location.distance_to_point(cp.position) > EVENT_DEPARTURE_MAX_DISTANCE: + return False + + if cp.is_global and not self.global_cp_available: + return False + + return True + def bonus(self) -> int: return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE) def is_successfull(self, debriefing: Debriefing) -> bool: return self.operation.is_successfull(debriefing) - def player_attacking(self, flights: db.TaskForceDict): - assert False + def player_attacking(self, cp: ControlPoint, flights: db.TaskForceDict): + if self.is_player_attacking: + self.departure_cp = cp + else: + self.to_cp = cp - def player_defending(self, flights: db.TaskForceDict): - assert False + def player_defending(self, cp: ControlPoint, flights: db.TaskForceDict): + if self.is_player_attacking: + self.departure_cp = cp + else: + self.to_cp = cp def generate(self): self.operation.is_awacs_enabled = self.is_awacs_enabled @@ -93,7 +129,7 @@ class Event: def commit(self, debriefing: Debriefing): for country, losses in debriefing.destroyed_units.items(): if country == self.attacker_name: - cp = self.from_cp + cp = self.departure_cp else: cp = self.to_cp @@ -122,11 +158,12 @@ class UnitsDeliveryEvent(Event): units = None # type: typing.Dict[UnitType, int] def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game): - super(UnitsDeliveryEvent, self).__init__(attacker_name=attacker_name, - defender_name=defender_name, + super(UnitsDeliveryEvent, self).__init__(game=game, + location=to_cp.position, from_cp=from_cp, - to_cp=to_cp, - game=game) + target_cp=to_cp, + attacker_name=attacker_name, + defender_name=defender_name) self.units = {} diff --git a/game/event/frontlineattack.py b/game/event/frontlineattack.py index a4a923b0..501a6a12 100644 --- a/game/event/frontlineattack.py +++ b/game/event/frontlineattack.py @@ -11,8 +11,6 @@ class FrontlineAttackEvent(Event): STRENGTH_INFLUENCE = 0.3 SUCCESS_FACTOR = 1.5 - defenders = None # type: db.ArmorDict - @property def threat_description(self): return "{} vehicles".format(self.to_cp.base.assemble_count()) @@ -20,9 +18,13 @@ class FrontlineAttackEvent(Event): @property def tasks(self) -> typing.Collection[typing.Type[Task]]: if self.is_player_attacking: - return [CAS, PinpointStrike] + return [CAS, CAP] else: - return [CAP, PinpointStrike] + return [CAP] + + @property + def global_cp_available(self) -> bool: + return True def flight_name(self, for_task: typing.Type[Task]) -> str: if for_task == CAS: @@ -36,8 +38,8 @@ class FrontlineAttackEvent(Event): return "Frontline attack" def is_successfull(self, debriefing: Debriefing): - alive_attackers = sum([v for k, v in debriefing.alive_units[self.attacker_name].items() if db.unit_task(k) == PinpointStrike]) - alive_defenders = sum([v for k, v in debriefing.alive_units[self.defender_name].items() if db.unit_task(k) == PinpointStrike]) + alive_attackers = sum([v for k, v in debriefing.alive_units.get(self.attacker_name, {}).items() if db.unit_task(k) == PinpointStrike]) + alive_defenders = sum([v for k, v in debriefing.alive_units.get(self.defender_name, {}).items() if db.unit_task(k) == PinpointStrike]) attackers_success = (float(alive_attackers) / (alive_defenders + 0.01)) > self.SUCCESS_FACTOR if self.from_cp.captured: return attackers_success @@ -63,20 +65,46 @@ class FrontlineAttackEvent(Event): self.to_cp.base.affect_strength(-0.1) def player_attacking(self, flights: db.TaskForceDict): - assert CAS in flights and PinpointStrike in flights and len(flights) == 2, "Invalid flights" - - self.defenders = self.to_cp.base.assemble_attack() + assert CAS in flights and CAP in flights and len(flights) == 2, "Invalid flights" op = FrontlineAttackOperation(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) - armor = unitdict_from(flights[PinpointStrike]) - op.setup(target=self.defenders, - attackers=db.unitdict_restrict_count(armor, sum(self.defenders.values())), - strikegroup=flights[CAS]) + defenders = self.to_cp.base.assemble_attack() + max_attackers = int(math.ceil(sum(defenders.values()) * self.ATTACKER_DEFENDER_FACTOR)) + attackers = db.unitdict_restrict_count(self.from_cp.base.assemble_attack(), max_attackers) + op.setup(defenders=defenders, + attackers=attackers, + strikegroup=flights[CAS], + escort=flights[CAP], + interceptors=assigned_units_from(self.to_cp.base.scramble_interceptors(1))) + + self.operation = op + + def player_defending(self, flights: db.TaskForceDict): + assert CAP in flights and len(flights) == 1, "Invalid flights" + + op = FrontlineAttackOperation(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) + + defenders = self.to_cp.base.assemble_attack() + + max_attackers = int(math.ceil(sum(defenders.values()))) + attackers = db.unitdict_restrict_count(self.from_cp.base.assemble_attack(), max_attackers) + + op.setup(defenders=defenders, + attackers=attackers, + strikegroup=assigned_units_from(self.from_cp.base.scramble_cas(1)), + escort=assigned_units_from(self.from_cp.base.scramble_sweep(1)), + interceptors=flights[CAP]) self.operation = op diff --git a/game/event/frontlinepatrol.py b/game/event/frontlinepatrol.py index ef891c3e..8cab0844 100644 --- a/game/event/frontlinepatrol.py +++ b/game/event/frontlinepatrol.py @@ -17,7 +17,7 @@ class FrontlinePatrolEvent(Event): @property def tasks(self): - return [CAP, PinpointStrike] + return [CAP] def flight_name(self, for_task: typing.Type[Task]) -> str: if for_task == CAP: @@ -55,7 +55,7 @@ class FrontlinePatrolEvent(Event): pass def player_attacking(self, flights: db.TaskForceDict): - assert CAP in flights and PinpointStrike in flights and len(flights) == 2, "Invalid flights" + assert CAP in flights and len(flights) == 1, "Invalid flights" self.cas = self.to_cp.base.scramble_cas(self.game.settings.multiplier) self.escort = self.to_cp.base.scramble_sweep(self.game.settings.multiplier * self.ESCORT_FACTOR) @@ -64,13 +64,15 @@ class FrontlinePatrolEvent(Event): 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) defenders = self.to_cp.base.assemble_attack() + attackers = db.unitdict_restrict_count(self.from_cp.base.assemble_attack(), sum(defenders.values())) op.setup(cas=assigned_units_from(self.cas), escort=assigned_units_from(self.escort), interceptors=flights[CAP], - armor_attackers=db.unitdict_restrict_count(db.unitdict_from(flights[PinpointStrike]), sum(defenders.values())), + armor_attackers=attackers, armor_defenders=defenders) self.operation = op diff --git a/game/event/infantrytransport.py b/game/event/infantrytransport.py index 59d85abe..0ebebd06 100644 --- a/game/event/infantrytransport.py +++ b/game/event/infantrytransport.py @@ -35,7 +35,7 @@ class InfantryTransportEvent(Event): if self.is_successfull(debriefing): self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) else: - self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) + self.departure_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) def player_attacking(self, flights: db.TaskForceDict): assert Embarking in flights and len(flights) == 1, "Invalid flights" @@ -45,6 +45,7 @@ class InfantryTransportEvent(Event): 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 ) diff --git a/game/event/insurgentattack.py b/game/event/insurgentattack.py index a04d2168..e98b1e7f 100644 --- a/game/event/insurgentattack.py +++ b/game/event/insurgentattack.py @@ -57,6 +57,7 @@ class InsurgentAttackEvent(Event): 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]) diff --git a/game/event/intercept.py b/game/event/intercept.py index a137d9b2..9c14e753 100644 --- a/game/event/intercept.py +++ b/game/event/intercept.py @@ -10,6 +10,11 @@ class InterceptEvent(Event): transport_unit = None # type: FlyingType + def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, + defender_name: str): + super().__init__(game, from_cp, target_cp, location, attacker_name, defender_name) + self.location = Conflict.intercept_position(self.from_cp, self.to_cp) + def __str__(self): return "Air Intercept" @@ -25,15 +30,19 @@ class InterceptEvent(Event): return "Escort flight" def _enemy_scramble_multiplier(self) -> float: - is_global = self.from_cp.is_global or self.to_cp.is_global + is_global = self.departure_cp.is_global or self.to_cp.is_global return self.game.settings.multiplier * is_global and 0.5 or 1 @property def threat_description(self): return "{} aircraft".format(self.enemy_cp.base.scramble_count(self._enemy_scramble_multiplier(), CAP)) + @property + def global_cp_available(self) -> bool: + return True + def is_successfull(self, debriefing: Debriefing): - units_destroyed = debriefing.destroyed_units[self.defender_name].get(self.transport_unit, 0) + units_destroyed = debriefing.destroyed_units.get(self.defender_name, {}).get(self.transport_unit, 0) if self.from_cp.captured: return units_destroyed > 0 else: @@ -72,9 +81,11 @@ class InterceptEvent(Event): 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(escort=assigned_units_from(escort), + op.setup(location=self.location, + escort=assigned_units_from(escort), transport={self.transport_unit: 1}, airdefense={airdefense_unit: self.AIRDEFENSE_COUNT}, interceptors=flights[CAP]) @@ -93,9 +104,11 @@ class InterceptEvent(Event): 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(escort=flights[CAP], + op.setup(location=self.location, + escort=flights[CAP], transport={self.transport_unit: 1}, interceptors=assigned_units_from(interceptors), airdefense={}) diff --git a/game/event/navalintercept.py b/game/event/navalintercept.py index df425415..f29bcb8a 100644 --- a/game/event/navalintercept.py +++ b/game/event/navalintercept.py @@ -9,6 +9,11 @@ class NavalInterceptEvent(Event): targets = None # type: db.ShipDict + def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, + defender_name: str): + super().__init__(game, from_cp, target_cp, location, attacker_name, defender_name) + self.location = Conflict.naval_intercept_position(from_cp, target_cp, game.theater) + def _targets_count(self) -> int: from gen.conflictgen import IMPORTANCE_LOW factor = (self.to_cp.importance - IMPORTANCE_LOW + 0.1) * 20 @@ -33,18 +38,22 @@ class NavalInterceptEvent(Event): @property def threat_description(self): s = "{} ship(s)".format(self._targets_count()) - if not self.from_cp.captured: - s += ", {} aircraft".format(self.from_cp.base.scramble_count(self.game.settings.multiplier)) + if not self.departure_cp.captured: + s += ", {} aircraft".format(self.departure_cp.base.scramble_count(self.game.settings.multiplier)) return s + @property + def global_cp_available(self) -> bool: + return True + 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(): + for unit, count in debriefing.destroyed_units.get(self.defender_name, {}).items(): if unit in self.targets: destroyed_targets += count - if self.from_cp.captured: + if self.departure_cp.captured: return math.ceil(float(destroyed_targets) / total_targets) > self.SUCCESS_RATE else: return math.ceil(float(destroyed_targets) / total_targets) < self.SUCCESS_RATE @@ -56,11 +65,11 @@ class NavalInterceptEvent(Event): if self.is_successfull(debriefing): self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) else: - self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) + self.departure_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) else: # enemy attacking if self.is_successfull(debriefing): - self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) + self.departure_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) else: self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE) @@ -80,10 +89,12 @@ class NavalInterceptEvent(Event): 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(strikegroup=flights[CAS], + op.setup(location=self.location, + strikegroup=flights[CAS], interceptors={}, targets=self.targets) @@ -101,10 +112,11 @@ class NavalInterceptEvent(Event): 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 ) - strikegroup = self.from_cp.base.scramble_cas(self.game.settings.multiplier) + strikegroup = self.departure_cp.base.scramble_cas(self.game.settings.multiplier) op.setup(strikegroup=assigned_units_from(strikegroup), interceptors=flights[CAP], targets=self.targets) diff --git a/game/event/strike.py b/game/event/strike.py index e8bea076..9fc35bcc 100644 --- a/game/event/strike.py +++ b/game/event/strike.py @@ -8,7 +8,7 @@ class StrikeEvent(Event): SINGLE_OBJECT_STRENGTH_INFLUENCE = 0.05 def __str__(self): - return "Strike" + return "Strike / SEAD" def is_successfull(self, debriefing: Debriefing): return True @@ -20,7 +20,7 @@ class StrikeEvent(Event): @property def tasks(self): if self.is_player_attacking: - return [CAP, CAS] + return [CAP, CAS, SEAD] else: return [CAP] @@ -28,12 +28,22 @@ class StrikeEvent(Event): def ai_banned_tasks(self): return [CAS] + @property + def player_banned_tasks(self): + return [SEAD] + + @property + def global_cp_available(self) -> bool: + return True + def flight_name(self, for_task: typing.Type[Task]) -> str: if for_task == CAP: if self.is_player_attacking: return "Escort flight" else: return "CAP flight" + elif for_task == SEAD: + return "SEAD flight" elif for_task == CAS: return "Strike flight" @@ -43,18 +53,20 @@ class StrikeEvent(Event): self.to_cp.base.affect_strength(-self.SINGLE_OBJECT_STRENGTH_INFLUENCE * len(debriefing.destroyed_objects)) def player_attacking(self, flights: db.TaskForceDict): - assert CAP in flights and CAS in flights and len(flights) == 2, "Invalid flights" + assert CAP in flights and CAS in flights and SEAD in flights and len(flights) == 3, "Invalid flights" op = StrikeOperation( 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 ) interceptors = self.to_cp.base.scramble_interceptors(self.game.settings.multiplier) op.setup(strikegroup=flights[CAS], + sead=flights[SEAD], escort=flights[CAP], interceptors=assigned_units_from(interceptors)) diff --git a/game/game.py b/game/game.py index f3f003a8..356e3375 100644 --- a/game/game.py +++ b/game/game.py @@ -43,19 +43,19 @@ Events: * BaseAttackEvent - capture base * InterceptEvent - air intercept * FrontlineAttackEvent - frontline attack -* FrontlineCAPEvent - frontline attack * NavalInterceptEvent - naval intercept * StrikeEvent - strike event * InfantryTransportEvent - helicopter infantry transport """ EVENT_PROBABILITIES = { # events always present; only for the player - FrontlineAttackEvent: [100, 0], - FrontlinePatrolEvent: [100, 0], + FrontlineAttackEvent: [100, 9], + #FrontlinePatrolEvent: [100, 0], StrikeEvent: [100, 0], # events randomly present; only for the player - InfantryTransportEvent: [25, 0], + #InfantryTransportEvent: [25, 0], + ConvoyStrikeEvent: [25, 0], # events conditionally present; for both enemy and player BaseAttackEvent: [100, 9], @@ -100,28 +100,18 @@ class Game: self.enemy = enemy_name def _roll(self, prob, mult): - return random.randint(1, 100) <= prob * mult - - def _generate_globalinterceptions(self): - global_count = len([x for x in self.theater.player_points() if x.is_global]) - for from_cp in [x for x in self.theater.player_points() if x.is_global]: - probability_base = max(PLAYER_INTERCEPT_GLOBAL_PROBABILITY_BASE / global_count, 1) - probability = probability_base * math.log(len(self.theater.player_points()) + 1, PLAYER_INTERCEPT_GLOBAL_PROBABILITY_LOG) - if self._roll(probability, from_cp.base.strength): - to_cp = random.choice([x for x in self.theater.enemy_points() if x not in self.theater.conflicts()]) - self.events.append(InterceptEvent(attacker_name=self.player, - defender_name=self.enemy, - from_cp=from_cp, - to_cp=to_cp, - game=self)) - break + if self.settings.version == "dev": + # always generate all events for dev + return 100 + else: + return random.randint(1, 100) <= prob * mult def _generate_player_event(self, event_class, player_cp, enemy_cp): if event_class == NavalInterceptEvent and enemy_cp.radials == LAND: # skip naval events for non-coastal CPs return - if event_class == BaseAttackEvent and enemy_cp.base.strength > PLAYER_BASEATTACK_THRESHOLD: + if event_class == BaseAttackEvent and enemy_cp.base.strength > PLAYER_BASEATTACK_THRESHOLD and self.settings.version != "dev": # skip base attack events for CPs yet too strong return @@ -129,7 +119,7 @@ class Game: # skip strikes in case of no targets return - self.events.append(event_class(self.player, self.enemy, player_cp, enemy_cp, self)) + self.events.append(event_class(self, player_cp, enemy_cp, enemy_cp.position, self.player, self.enemy)) def _generate_enemy_event(self, event_class, player_cp, enemy_cp): if event_class in [type(x) for x in self.events if not self.is_player_attack(x)]: @@ -169,28 +159,35 @@ class Game: # skip base attack if strength is too high return - self.events.append(event_class(self.enemy, self.player, enemy_cp, player_cp, self)) + self.events.append(event_class(self, enemy_cp, player_cp, player_cp.position, self.enemy, self.player)) def _generate_events(self): - for player_cp, enemy_cp in self.theater.conflicts(True): - if enemy_cp.is_global: - continue + strikes_generated_for = set() + base_attack_generated_for = set() + 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 - if player_cp.is_global: - # skip events requiring ground CP - if event_class not in [InterceptEvent, StrikeEvent, NavalInterceptEvent]: + # don't generate multiple 100% events from each attack direction + if event_class is StrikeEvent: + if enemy_cp in strikes_generated_for: + continue + if event_class is BaseAttackEvent: + if enemy_cp in base_attack_generated_for: continue - if player_probability == 100 or self._roll(player_probability, player_cp.base.strength): + if player_probability == 100 or player_probability > 0 and self._roll(player_probability, player_cp.base.strength): self._generate_player_event(event_class, player_cp, enemy_cp) + if event_class is StrikeEvent: + strikes_generated_for.add(enemy_cp) + if event_class is BaseAttackEvent: + base_attack_generated_for.add(enemy_cp) - if enemy_probability == 100 or self._roll(enemy_probability, enemy_cp.base.strength): + if enemy_probability == 100 or enemy_probability > 0 and self._roll(enemy_probability, enemy_cp.base.strength): self._generate_enemy_event(event_class, player_cp, enemy_cp) def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> typing.Collection[UnitType]: @@ -269,7 +266,12 @@ class Game: def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint]=None): logging.info("Pass turn") for event in self.events: - event.skip() + if self.settings.version == "dev": + # don't damage player CPs in by skipping in dev mode + if isinstance(event, UnitsDeliveryEvent): + event.skip() + else: + event.skip() if not no_action: self._budget_player() @@ -286,5 +288,5 @@ class Game: self.events = [] # type: typing.List[Event] self._generate_events() - self._generate_globalinterceptions() + #self._generate_globalinterceptions() diff --git a/game/operation/convoystrike.py b/game/operation/convoystrike.py new file mode 100644 index 00000000..c38a2eed --- /dev/null +++ b/game/operation/convoystrike.py @@ -0,0 +1,49 @@ +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): + if self.is_player_attack: + self.prepare_carriers(db.unitdict_from(self.strikegroup)) + + 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/game/operation/frontlineattack.py b/game/operation/frontlineattack.py index bc131a22..a9272cb6 100644 --- a/game/operation/frontlineattack.py +++ b/game/operation/frontlineattack.py @@ -7,16 +7,24 @@ MAX_DISTANCE_BETWEEN_GROUPS = 12000 class FrontlineAttackOperation(Operation): + interceptors = None # type: db.AssignedUnitsDict + escort = None # type: db.AssignedUnitsDict strikegroup = None # type: db.AssignedUnitsDict + attackers = None # type: db.ArmorDict - target = None # type: db.ArmorDict + defenders = None # type: db.ArmorDict def setup(self, - target: db.ArmorDict, + defenders: db.ArmorDict, attackers: db.ArmorDict, - strikegroup: db.AssignedUnitsDict): + strikegroup: db.AssignedUnitsDict, + escort: db.AssignedUnitsDict, + interceptors: db.AssignedUnitsDict): self.strikegroup = strikegroup - self.target = target + self.escort = escort + self.interceptors = interceptors + + self.defenders = defenders self.attackers = attackers def prepare(self, terrain: Terrain, is_quick: bool): @@ -37,8 +45,13 @@ class FrontlineAttackOperation(Operation): conflict=conflict) def generate(self): - self.armorgen.generate_vec(self.attackers, self.target) + if self.is_player_attack: + self.prepare_carriers(db.unitdict_from(self.strikegroup)) + # ground units + self.armorgen.generate_vec(self.attackers, self.defenders) + + # strike group w/ heli support 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) @@ -51,6 +64,10 @@ class FrontlineAttackOperation(Operation): at=farp, escort=len(planes_flights) == 0) + self.airgen.generate_attackers_escort(*assigned_units_split(self.escort), at=self.attackers_starting_position) + + self.airgen.generate_defense(*assigned_units_split(self.interceptors), at=self.defenders_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." self.briefinggen.append_waypoint("CAS AREA IP") diff --git a/game/operation/frontlinepatrol.py b/game/operation/frontlinepatrol.py index 877b8299..631d89fe 100644 --- a/game/operation/frontlinepatrol.py +++ b/game/operation/frontlinepatrol.py @@ -43,6 +43,9 @@ class FrontlinePatrolOperation(Operation): conflict=conflict) def generate(self): + if self.is_player_attack: + self.prepare_carriers(db.unitdict_from(self.interceptors)) + self.airgen.generate_defenders_cas(*assigned_units_split(self.cas), at=self.defenders_starting_position) self.airgen.generate_defenders_escort(*assigned_units_split(self.escort), at=self.defenders_starting_position) self.airgen.generate_migcap(*assigned_units_split(self.interceptors), at=self.attackers_starting_position) diff --git a/game/operation/intercept.py b/game/operation/intercept.py index 14d4e344..19ac6b72 100644 --- a/game/operation/intercept.py +++ b/game/operation/intercept.py @@ -4,6 +4,7 @@ from .operation import * class InterceptOperation(Operation): + location = None # type: Point escort = None # type: db.AssignedUnitsDict transport = None # type: db.PlaneDict interceptors = None # type: db.AssignedUnitsDict @@ -12,10 +13,12 @@ class InterceptOperation(Operation): trigger_radius = TRIGGER_RADIUS_LARGE def setup(self, + location: Point, escort: db.AssignedUnitsDict, transport: db.PlaneDict, airdefense: db.AirDefenseDict, interceptors: db.AssignedUnitsDict): + self.location = location self.escort = escort self.transport = transport self.airdefense = airdefense @@ -30,6 +33,7 @@ class InterceptOperation(Operation): conflict = Conflict.intercept_conflict( attacker=self.current_mission.country(self.attacker_name), defender=self.current_mission.country(self.defender_name), + position=self.location, from_cp=self.from_cp, to_cp=self.to_cp, theater=self.game.theater @@ -39,7 +43,8 @@ class InterceptOperation(Operation): conflict=conflict) def generate(self): - self.prepare_carriers(db.unitdict_from(self.interceptors)) + if self.is_player_attack: + self.prepare_carriers(db.unitdict_from(self.interceptors)) self.airgen.generate_transport(self.transport, self.to_cp.at) self.airgen.generate_defenders_escort(*assigned_units_split(self.escort), at=self.defenders_starting_position) diff --git a/game/operation/navalintercept.py b/game/operation/navalintercept.py index b4ab9157..5c30581d 100644 --- a/game/operation/navalintercept.py +++ b/game/operation/navalintercept.py @@ -4,15 +4,18 @@ from .operation import * class NavalInterceptionOperation(Operation): + location = None # type: Point strikegroup = None # type: db.AssignedUnitsDict interceptors = None # type: db.AssignedUnitsDict targets = None # type: db.ShipDict trigger_radius = TRIGGER_RADIUS_LARGE def setup(self, + location: Point, strikegroup: db.AssignedUnitsDict, interceptors: db.AssignedUnitsDict, targets: db.ShipDict): + self.location = location self.strikegroup = strikegroup self.interceptors = interceptors self.targets = targets @@ -25,6 +28,7 @@ class NavalInterceptionOperation(Operation): conflict = Conflict.naval_intercept_conflict( attacker=self.current_mission.country(self.attacker_name), defender=self.current_mission.country(self.defender_name), + position=self.location, from_cp=self.from_cp, to_cp=self.to_cp, theater=self.game.theater @@ -33,7 +37,8 @@ class NavalInterceptionOperation(Operation): self.initialize(self.current_mission, conflict) def generate(self): - self.prepare_carriers(db.unitdict_from(self.strikegroup)) + if self.is_player_attack: + self.prepare_carriers(db.unitdict_from(self.strikegroup)) target_groups = self.shipgen.generate_cargo(units=self.targets) diff --git a/game/operation/operation.py b/game/operation/operation.py index 509228da..b0851309 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -26,6 +26,7 @@ class Operation: envgen = None # type: EnvironmentGenerator groundobjectgen = None # type: GroundObjectsGenerator briefinggen = None # type: BriefingGenerator + forcedoptionsgen = None # type: ForcedOptionsGenerator environment_settings = None trigger_radius = TRIGGER_RADIUS_MEDIUM @@ -38,11 +39,13 @@ class Operation: attacker_name: str, defender_name: str, from_cp: ControlPoint, + departure_cp: ControlPoint, to_cp: ControlPoint = None): self.game = game self.attacker_name = attacker_name self.defender_name = defender_name self.from_cp = from_cp + self.departure_cp = departure_cp self.to_cp = to_cp self.is_quick = False @@ -52,6 +55,10 @@ class Operation: def is_successfull(self, debriefing: Debriefing) -> bool: return True + @property + def is_player_attack(self) -> bool: + return self.from_cp.captured + def initialize(self, mission: Mission, conflict: Conflict): self.current_mission = mission self.conflict = conflict @@ -63,6 +70,7 @@ class Operation: self.triggersgen = TriggersGenerator(mission, conflict, self.game) self.visualgen = VisualGenerator(mission, conflict, self.game) self.envgen = EnviromentGenerator(mission, conflict, self.game) + self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game) self.groundobjectgen = GroundObjectsGenerator(mission, conflict, self.game) self.briefinggen = BriefingGenerator(mission, conflict, self.game) @@ -87,24 +95,24 @@ class Operation: self.attackers_starting_position = None self.defenders_starting_position = None else: - self.attackers_starting_position = self.from_cp.at + self.attackers_starting_position = self.departure_cp.at self.defenders_starting_position = self.to_cp.at def prepare_carriers(self, for_units: db.UnitsDict): - for global_cp in self.game.theater.controlpoints: - if not global_cp.is_global: - continue + if not self.departure_cp.is_global: + return - ship = self.shipgen.generate_carrier(for_units=[t for t, c in for_units.items() if c > 0], - country=self.game.player, - at=global_cp.at) + ship = self.shipgen.generate_carrier(for_units=[t for t, c in for_units.items() if c > 0], + country=self.game.player, + at=self.departure_cp.at) - if global_cp == self.from_cp and not self.is_quick: + if not self.is_quick: + if not self.to_cp.captured: self.attackers_starting_position = ship + else: + self.defenders_starting_position = ship def generate(self): - self.visualgen.generate() - # air support self.airsupportgen.generate(self.is_awacs_enabled) for i, tanker_type in enumerate(self.airsupportgen.generated_tankers): @@ -141,10 +149,17 @@ class Operation: else: self.envgen.load(self.environment_settings) + # options + self.forcedoptionsgen.generate() + # main frequencies self.briefinggen.append_frequency("Flight", "251 MHz AM") - if self.conflict.from_cp.is_global or self.conflict.to_cp.is_global: + if self.departure_cp.is_global or self.conflict.to_cp.is_global: self.briefinggen.append_frequency("Carrier", "20X/ICLS CHAN1") # briefing self.briefinggen.generate() + + # visuals + self.visualgen.generate() + diff --git a/game/operation/strike.py b/game/operation/strike.py index 9c158f13..84b4eed5 100644 --- a/game/operation/strike.py +++ b/game/operation/strike.py @@ -5,6 +5,7 @@ from .operation import * class StrikeOperation(Operation): strikegroup = None # type: db.AssignedUnitsDict + sead = None # type: db.AssignedUnitsDict escort = None # type: db.AssignedUnitsDict interceptors = None # type: db.AssignedUnitsDict @@ -12,9 +13,11 @@ class StrikeOperation(Operation): def setup(self, strikegroup: db.AssignedUnitsDict, + sead: db.AssignedUnitsDict, escort: db.AssignedUnitsDict, interceptors: db.AssignedUnitsDict): self.strikegroup = strikegroup + self.sead = sead self.escort = escort self.interceptors = interceptors @@ -40,8 +43,10 @@ class StrikeOperation(Operation): self.prepare_carriers(db.unitdict_merge(db.unitdict_from(self.strikegroup), db.unitdict_from(self.escort))) targets = [] # type: typing.List[typing.Tuple[str, str, Point]] + sead_targets = [] # type: typing.List[typing.Tuple[str, str, Point]] category_counters = {} # type: typing.Dict[str, int] processed_groups = [] + for object in self.to_cp.ground_objects: if object.group_identifier in processed_groups: continue @@ -49,6 +54,10 @@ class StrikeOperation(Operation): processed_groups.append(object.group_identifier) category_counters[object.category] = category_counters.get(object.category, 0) + 1 markpoint_name = "{}{}".format(object.name_abbrev, category_counters[object.category]) + + if object.category == "aa": + sead_targets.append((str(object), markpoint_name, object.position)) + targets.append((str(object), markpoint_name, object.position)) targets.sort(key=lambda x: self.from_cp.position.distance_to_point(x[2])) @@ -59,7 +68,13 @@ class StrikeOperation(Operation): planes_flights = {k: v for k, v in self.strikegroup.items() if k in plane_map.values()} self.airgen.generate_ground_attack_strikegroup(*assigned_units_split(planes_flights), targets=[(mp, pos) for (n, mp, pos) in targets], - at=self.attackers_starting_position) + at=self.attackers_starting_position, + escort=len(self.sead) == 0) + + self.airgen.generate_sead_strikegroup(*assigned_units_split(self.sead), + targets=[(mp, pos) for (n, mp, pos) in sead_targets], + at=self.attackers_starting_position, + escort=len(self.sead) > 0) heli_flights = {k: v for k, v in self.strikegroup.items() if k in helicopters.helicopter_map.values()} if heli_flights: diff --git a/game/settings.py b/game/settings.py index 36edefd1..81f377aa 100644 --- a/game/settings.py +++ b/game/settings.py @@ -3,6 +3,8 @@ class Settings: player_skill = "Good" enemy_skill = "Average" enemy_vehicle_skill = "Average" + map_coalition_visibility = "All Units" + labels = "Full" only_player_takeoff = True night_disabled = False diff --git a/gen/__init__.py b/gen/__init__.py index bbbf3bd0..ccddeab9 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -9,6 +9,7 @@ from .triggergen import * from .environmentgen import * from .groundobjectsgen import * from .briefinggen import * +from .forcedoptionsgen import * from . import naming diff --git a/gen/aircraft.py b/gen/aircraft.py index 70aed59e..33fd2aff 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -17,23 +17,23 @@ ESCORT_ENGAGEMENT_MAX_DIST = 100000 WORKAROUND_WAYP_DIST = 1000 WARM_START_HELI_AIRSPEED = 120 -WARM_START_HELI_ALT = 1000 +WARM_START_HELI_ALT = 500 WARM_START_ALTITUDE = 3000 WARM_START_AIRSPEED = 550 -INTERCEPTION_ALT = 3000 INTERCEPTION_AIRSPEED = 1000 BARCAP_RACETRACK_DISTANCE = 20000 -ATTACK_CIRCLE_ALT = 5000 +ATTACK_CIRCLE_ALT = 1000 ATTACK_CIRCLE_DURATION = 15 -CAS_ALTITUDE = 1000 -RTB_ALTITUDE = 1000 -HELI_ALT = 900 +CAS_ALTITUDE = 800 +RTB_ALTITUDE = 800 +RTB_DISTANCE = 5000 +HELI_ALT = 500 -TRANSPORT_LANDING_ALT = 1000 +TRANSPORT_LANDING_ALT = 2000 DEFENCE_ENGAGEMENT_MAX_DISTANCE = 60000 INTERCEPT_MAX_DISTANCE = 200000 @@ -69,9 +69,15 @@ class AircraftConflictGenerator: else: client_count = 0 + if flying_type == F_14B: + # workaround since 2 and 3 tomcat collide on carrier + group_size = 2 + else: + group_size = 4 + while count > 0: - group_size = min(count, 4) - client_size = max(min(client_count, 4), 0) + group_size = min(count, group_size) + client_size = max(min(client_count, group_size), 0) yield (flying_type, group_size, client_size) count -= group_size @@ -149,7 +155,7 @@ class AircraftConflictGenerator: pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000)) logging.info("airgen: {} for {} at {} at {}".format(unit_type, side.id, alt, speed)) - return self.m.flight_group( + group = self.m.flight_group( country=side, name=name, aircraft_type=unit_type, @@ -161,6 +167,9 @@ class AircraftConflictGenerator: start_type=self._start_type(), group_size=count) + group.points[0].alt_type = "RADIO" + return group + def _generate_at_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: typing.Union[ShipGroup, StaticGroup]) -> FlyingGroup: assert count > 0 assert unit is not None @@ -180,7 +189,9 @@ class AircraftConflictGenerator: return self._generate_inflight(name, side, unit_type, count, client_count, at) elif isinstance(at, Group): takeoff_ban = unit_type in db.CARRIER_TAKEOFF_BAN - if not takeoff_ban: + ai_ban = client_count == 0 and self.settings.only_player_takeoff + + if not takeoff_ban and not ai_ban: return self._generate_at_group(name, side, unit_type, count, client_count, at) else: return self._generate_inflight(name, side, unit_type, count, client_count, at.position) @@ -197,17 +208,26 @@ class AircraftConflictGenerator: else: assert False + def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600): + point = group.add_waypoint(position, altitude, airspeed) + point.alt_type = "RADIO" + return point + def _rtb_for(self, group: FlyingGroup, cp: ControlPoint, at: db.StartingPosition = None): if not at: at = cp.at + position = at if isinstance(at, Point) else at.position - if isinstance(at, Point): - group.add_waypoint(at, RTB_ALTITUDE) - elif isinstance(at, Group): - group.add_waypoint(at.position, RTB_ALTITUDE) - elif issubclass(at, Airport): - group.add_waypoint(at.position, RTB_ALTITUDE) + last_waypoint = group.points[-1] + if last_waypoint is not None: + heading = position.heading_between_point(last_waypoint.position) + tod_location = position.point_from_heading(heading, RTB_DISTANCE) + self._add_radio_waypoint(group, tod_location, last_waypoint.alt) + + destination_waypoint = self._add_radio_waypoint(group, position, RTB_ALTITUDE) + if isinstance(at, Airport): group.land_at(at) + return destination_waypoint def _at_position(self, at) -> Point: if isinstance(at, Point): @@ -244,7 +264,7 @@ class AircraftConflictGenerator: orbit_task = ControlledTask(OrbitAction(ATTACK_CIRCLE_ALT, pattern=OrbitAction.OrbitPattern.Circle)) orbit_task.stop_after_duration(ATTACK_CIRCLE_DURATION * 60) - orbit_waypoint = group.add_waypoint(self.conflict.position, CAS_ALTITUDE) + orbit_waypoint = self._add_radio_waypoint(group, self.conflict.position, CAS_ALTITUDE) orbit_waypoint.tasks.append(orbit_task) orbit_waypoint.tasks.append(EngageTargets(max_distance=DEFENCE_ENGAGEMENT_MAX_DISTANCE)) @@ -263,9 +283,9 @@ class AircraftConflictGenerator: client_count=client_count, at=at and at or self._group_point(self.conflict.air_attackers_location)) - waypoint = group.add_waypoint(self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED) + waypoint = self._add_radio_waypoint(group, self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED) if self.conflict.is_vector: - group.add_waypoint(self.conflict.tail, CAS_ALTITUDE, WARM_START_AIRSPEED) + self._add_radio_waypoint(group, self.conflict.tail, CAS_ALTITUDE, WARM_START_AIRSPEED) group.task = CAS.name self._setup_group(group, CAS, client_count) @@ -289,6 +309,7 @@ class AircraftConflictGenerator: for name, pos in targets: waypoint = group.add_waypoint(pos, 0, WARM_START_AIRSPEED, self.m.translation.create_string(name)) + waypoint.tasks.append(Bombing(pos, attack_qty=2)) if escort_until_waypoint is None: escort_until_waypoint = waypoint @@ -298,6 +319,32 @@ class AircraftConflictGenerator: self.escort_targets.append((group, group.points.index(escort_until_waypoint))) self._rtb_for(group, self.conflict.from_cp, at) + def generate_sead_strikegroup(self, strikegroup: db.PlaneDict, clients: db.PlaneDict, targets: typing.List[typing.Tuple[str, Point]], at: db.StartingPosition, escort=True): + assert not escort or 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, 0, WARM_START_AIRSPEED, self.m.translation.create_string(name)) + if escort_until_waypoint is None: + escort_until_waypoint = waypoint + + group.task = SEAD.name + self._setup_group(group, SEAD, client_count) + if escort: + 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, escort=True): assert not escort or len(self.escort_targets) == 0 @@ -312,11 +359,11 @@ class AircraftConflictGenerator: location = self._group_point(self.conflict.air_defenders_location) insertion_point = self.conflict.find_insertion_point(location) - waypoint = group.add_waypoint(insertion_point, CAS_ALTITUDE, WARM_START_AIRSPEED) + waypoint = self._add_radio_waypoint(group, insertion_point, CAS_ALTITUDE, WARM_START_AIRSPEED) if self.conflict.is_vector: destination_tail = self.conflict.tail.distance_to_point(insertion_point) > self.conflict.position.distance_to_point(insertion_point) - group.add_waypoint(destination_tail and self.conflict.tail or self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED) + self._add_radio_waypoint(group, destination_tail and self.conflict.tail or self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED) group.task = CAS.name self._setup_group(group, CAS, client_count) @@ -336,7 +383,7 @@ class AircraftConflictGenerator: client_count=client_count, at=at and at or self._group_point(self.conflict.air_attackers_location)) - wayp = group.add_waypoint(self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED) + wayp = self._add_radio_waypoint(group, self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED) for target_group in target_groups: wayp.tasks.append(AttackGroup(target_group.id)) @@ -377,7 +424,7 @@ class AircraftConflictGenerator: at=at and at or self._group_point(self.conflict.air_defenders_location)) group.task = CAP.name - wayp = group.add_waypoint(self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED) + wayp = self._add_radio_waypoint(group, self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED) wayp.tasks.append(dcs.task.EngageTargets(max_distance=DEFENCE_ENGAGEMENT_MAX_DISTANCE)) wayp.tasks.append(dcs.task.OrbitAction(ATTACK_CIRCLE_ALT, pattern=OrbitAction.OrbitPattern.Circle)) self._setup_group(group, CAP, client_count) @@ -393,9 +440,9 @@ class AircraftConflictGenerator: client_count=client_count, at=at and at or self._group_point(self.conflict.air_attackers_location)) - waypoint = group.add_waypoint(self.conflict.position, WARM_START_ALTITUDE, WARM_START_AIRSPEED) + waypoint = self._add_radio_waypoint(group, 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) + self._add_radio_waypoint(group, self.conflict.tail, WARM_START_ALTITUDE, WARM_START_AIRSPEED) group.task = CAP.name self._setup_group(group, CAP, client_count) @@ -411,14 +458,14 @@ class AircraftConflictGenerator: 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) + waypoint = self._add_radio_waypoint(group, 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) + self._add_radio_waypoint(group, self.conflict.tail, WARM_START_ALTITUDE, WARM_START_AIRSPEED) else: heading = group.position.heading_between_point(self.conflict.position) - waypoint = group.add_waypoint(self.conflict.position.point_from_heading(heading, BARCAP_RACETRACK_DISTANCE), - WARM_START_ALTITUDE, - WARM_START_AIRSPEED) + waypoint = self._add_radio_waypoint(group, self.conflict.position.point_from_heading(heading, BARCAP_RACETRACK_DISTANCE), + WARM_START_ALTITUDE, + WARM_START_AIRSPEED) waypoint.tasks.append(OrbitAction(WARM_START_ALTITUDE, WARM_START_AIRSPEED)) group.task = CAP.name @@ -437,9 +484,11 @@ class AircraftConflictGenerator: client_count=client_count, at=self._group_point(self.conflict.air_defenders_location)) - waypoint = group.add_waypoint(destination.position.random_point_within(0, 0), TRANSPORT_LANDING_ALT) + waypoint = self._rtb_for(group, self.conflict.to_cp) if escort: self.escort_targets.append((group, group.points.index(waypoint))) + + self._add_radio_waypoint(group, destination.position, RTB_ALTITUDE) group.task = Transport.name group.land_at(destination) @@ -456,11 +505,11 @@ class AircraftConflictGenerator: group.task = CAP.name group.points[0].tasks.append(EngageTargets(max_distance=INTERCEPT_MAX_DISTANCE)) - wayp = group.add_waypoint(self.conflict.position, WARM_START_ALTITUDE, INTERCEPTION_AIRSPEED) + wayp = self._add_radio_waypoint(group, self.conflict.position, WARM_START_ALTITUDE, INTERCEPTION_AIRSPEED) wayp.tasks.append(EngageTargets(max_distance=INTERCEPT_MAX_DISTANCE)) if self.conflict.is_vector: - group.add_waypoint(self.conflict.tail, CAS_ALTITUDE, WARM_START_ALTITUDE) + self._add_radio_waypoint(group, self.conflict.tail, CAS_ALTITUDE, WARM_START_ALTITUDE) self._setup_group(group, CAP, client_count) self._rtb_for(group, self.conflict.from_cp, at) @@ -476,9 +525,5 @@ class AircraftConflictGenerator: at=at and at or self._group_point(self.conflict.air_attackers_location) ) - group.add_waypoint( - pos=self.conflict.position, - altitude=HELI_ALT, - ) - + self._add_radio_waypoint(group, self.conflict.position, HELI_ALT) self._setup_group(group, Transport, client_count) diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 595e4fc7..caad2d49 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -49,12 +49,14 @@ class AirSupportConflictGenerator: ) tanker_group.points[0].tasks.append(ActivateBeaconCommand(channel=97 + i, unit_id=tanker_group.id, aa=False)) + tanker_group.points[0].tasks.append(SetInvisibleCommand(True)) + tanker_group.points[0].tasks.append(SetImmortalCommand(True)) if is_awacs_enabled: awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side.name)[0] - self.mission.awacs_flight( + awacs_flight = self.mission.awacs_flight( country=self.mission.country(self.game.player), - name=namegen.next_awacs_name(self.mission.country(self.game.player),), + name=namegen.next_awacs_name(self.mission.country(self.game.player)), plane_type=awacs_unit, altitude=AWACS_ALT, airport=None, @@ -62,3 +64,6 @@ class AirSupportConflictGenerator: frequency=133, start_type=StartType.Warm, ) + + awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) + awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) 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 f1f4f70e..775d0787 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -28,7 +28,7 @@ CAP_CAS_DISTANCE = 10000, 120000 GROUND_INTERCEPT_SPREAD = 5000 GROUND_DISTANCE_FACTOR = 1 -GROUND_DISTANCE = 4000 +GROUND_DISTANCE = 2000 GROUND_ATTACK_DISTANCE = 25000, 13000 @@ -162,6 +162,8 @@ class Conflict: strength_delta = (from_cp.base.strength - to_cp.base.strength) / 1.0 position = middle_point.point_from_heading(attack_heading, strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE) + return position, _opposite_heading(attack_heading) + ground_position = cls._find_ground_position(position, attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE, attack_heading, theater) if ground_position: return ground_position, _opposite_heading(attack_heading) @@ -172,6 +174,23 @@ class Conflict: @classmethod def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> typing.Optional[typing.Tuple[Point, int, int]]: + initial, heading = cls.frontline_position(theater, from_cp, to_cp) + + """ + probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH) + probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ]) + intersection = probe.intersection(theater.land_poly) + + if isinstance(intersection, geometry.LineString): + intersection = intersection + elif isinstance(intersection, geometry.MultiLineString): + intersection = intersection.geoms[0] + else: + print(intersection) + return None + + return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length + """ frontline = cls.frontline_position(theater, from_cp, to_cp) if not frontline: return None @@ -207,9 +226,21 @@ class Conflict: pos = new_pos else: return pos - return pos + """ + probe_end_point = initial.point_from_heading(heading, max_distance) + probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y)]) + + intersection = probe.intersection(theater.land_poly) + if intersection is geometry.LineString: + return Point(*intersection.xy[1]) + elif intersection is geometry.MultiLineString: + return Point(*intersection.geoms[0].xy[1]) + + return None + """ + @classmethod def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> typing.Optional[Point]: pos = initial @@ -218,9 +249,19 @@ class Conflict: return pos pos = pos.point_from_heading(heading, 500) + """ + probe_end_point = initial.point_from_heading(heading, max_distance) + probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ]) - logging.info("Didn't find ground position!") - return None + intersection = probe.intersection(theater.land_poly) + if isinstance(intersection, geometry.LineString): + return Point(*intersection.xy[1]) + elif isinstance(intersection, geometry.MultiLineString): + return Point(*intersection.geoms[0].xy[1]) + """ + + logging.error("Didn't find ground position ({})!".format(initial)) + return initial @classmethod def capture_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): @@ -277,13 +318,15 @@ class Conflict: ) @classmethod - def intercept_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): + def intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> Point: raw_distance = from_cp.position.distance_to_point(to_cp.position) * 1.5 distance = max(min(raw_distance, INTERCEPT_MAX_DISTANCE), INTERCEPT_MIN_DISTANCE) - heading = _heading_sum(from_cp.position.heading_between_point(to_cp.position), random.choice([-1, 1]) * random.randint(60, 100)) - position = from_cp.position.point_from_heading(heading, distance) + return from_cp.position.point_from_heading(heading, distance) + @classmethod + def intercept_conflict(cls, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): + heading = from_cp.position.heading_between_point(position) return cls( position=position.point_from_heading(position.heading_between_point(to_cp.position), INTERCEPT_CONFLICT_DISTANCE), theater=theater, @@ -319,6 +362,35 @@ 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 = frontline_heading + starting_position = Conflict._find_ground_position(frontline_position.point_from_heading(heading, 7000), + GROUND_INTERCEPT_SPREAD, + _opposite_heading(heading), theater) + if not starting_position: + starting_position = frontline_position + destination_position = frontline_position + else: + 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) @@ -385,7 +457,7 @@ class Conflict: ) @classmethod - def naval_intercept_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): + def naval_intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): radial = random.choice(to_cp.sea_radials) initial_distance = min(int(from_cp.position.distance_to_point(to_cp.position) * NAVAL_INTERCEPT_DISTANCE_FACTOR), NAVAL_INTERCEPT_DISTANCE_MAX) @@ -395,7 +467,10 @@ class Conflict: if not theater.is_on_land(position): break + return position + @classmethod + def naval_intercept_conflict(cls, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): attacker_heading = from_cp.position.heading_between_point(to_cp.position) return cls( position=position, diff --git a/gen/environmentgen.py b/gen/environmentgen.py index fb0600ef..98c1c3b4 100644 --- a/gen/environmentgen.py +++ b/gen/environmentgen.py @@ -26,9 +26,9 @@ WEATHER_FOG_VISIBILITY = 2500, 5000 WEATHER_FOG_THICKNESS = 100, 500 RANDOM_TIME = { - "night": 5, - "dusk": 30, - "dawn": 30, + "night": 7, + "dusk": 40, + "dawn": 40, "day": 100, } diff --git a/gen/forcedoptionsgen.py b/gen/forcedoptionsgen.py new file mode 100644 index 00000000..2022e3be --- /dev/null +++ b/gen/forcedoptionsgen.py @@ -0,0 +1,45 @@ +import logging +import typing +from enum import IntEnum + +from dcs.mission import Mission +from dcs.forcedoptions import ForcedOptions + +from .conflictgen import * + + +class Labels(IntEnum): + Off = 0 + Full = 1 + Abbreviated = 2 + Dot = 3 + + +class ForcedOptionsGenerator: + def __init__(self, mission: Mission, conflict: Conflict, game): + self.mission = mission + self.conflict = conflict + self.game = game + + def _set_options_view(self): + if self.game.settings.map_coalition_visibility == "All Units": + self.mission.forced_options.options_view = ForcedOptions.Views.All + elif self.game.settings.map_coalition_visibility == "Allied Units": + self.mission.forced_options.options_view = ForcedOptions.Views.Allies + elif self.game.settings.map_coalition_visibility == "Own Aircraft": + self.mission.forced_options.options_view = ForcedOptions.Views.MyAircraft + elif self.game.settings.map_coalition_visibility == "None": + self.mission.forced_options.options_view = ForcedOptions.Views.OnlyMap + + def _set_labels(self): + if self.game.settings.labels == "Abbreviated": + self.mission.forced_options.labels = int(Labels.Abbreviated) + elif self.game.settings.labels == "Dot Only": + self.mission.forced_options.labels = int(Labels.Dot) + elif self.game.settings.labels == "Off": + self.mission.forced_options.labels = int(Labels.Off) + + def generate(self): + self._set_options_view() + self._set_labels() + \ No newline at end of file diff --git a/gen/shipgen.py b/gen/shipgen.py index 47808961..d9695873 100644 --- a/gen/shipgen.py +++ b/gen/shipgen.py @@ -35,17 +35,19 @@ class ShipGenerator: def generate_cargo(self, units: db.ShipDict) -> typing.Collection[ShipGroup]: groups = [] + offset = 0 for unit_type, unit_count in units.items(): - logging.info("shipgen: {} ({}) for {}".format(unit_type, unit_count, self.conflict.defenders_side)) - group = self.m.ship_group( - country=self.conflict.defenders_side, - name=namegen.next_unit_name(self.conflict.defenders_side, unit_type), - _type=unit_type, - position=self.conflict.ground_defenders_location.random_point_within(SHIP_RANDOM_SPREAD, SHIP_RANDOM_SPREAD), - group_size=unit_count, - ) + for _ in range(unit_count): + offset += 1 + logging.info("shipgen: {} ({}) for {}".format(unit_type, unit_count, self.conflict.defenders_side)) + group = self.m.ship_group( + country=self.conflict.defenders_side, + name=namegen.next_unit_name(self.conflict.defenders_side, unit_type), + _type=unit_type, + position=self.conflict.ground_defenders_location.random_point_within(SHIP_RANDOM_SPREAD, SHIP_RANDOM_SPREAD).point_from_heading(0, offset * SHIP_RANDOM_SPREAD) + ) - group.add_waypoint(self.conflict.to_cp.position) - groups.append(group) + group.add_waypoint(self.conflict.to_cp.position) + groups.append(group) return groups diff --git a/gen/triggergen.py b/gen/triggergen.py index 1c4ecc9b..08abc9b1 100644 --- a/gen/triggergen.py +++ b/gen/triggergen.py @@ -54,14 +54,12 @@ class TriggersGenerator: vehicle_group.late_activation = True activate_by_trigger.append(vehicle_group) - """ conflict_distance = player_cp.position.distance_to_point(self.conflict.position) minimum_radius = max(conflict_distance - TRIGGER_MIN_DISTANCE_FROM_START, TRIGGER_RADIUS_MINIMUM) if minimum_radius < 0: minimum_radius = 0 - result_radius = min(minimum_radius, radius) - """ + radius = min(minimum_radius, radius) activation_trigger_zone = self.mission.triggers.add_triggerzone(self.conflict.position, radius, name="Activation zone") activation_trigger = TriggerOnce(Event.NoEvent, "Activation trigger") diff --git a/gen/visualgen.py b/gen/visualgen.py index 227af12f..16a848e9 100644 --- a/gen/visualgen.py +++ b/gen/visualgen.py @@ -9,6 +9,7 @@ from dcs.unit import Static from theater import * from .conflictgen import * #from game.game import Game +from game import db class MarkerSmoke(unittype.StaticType): @@ -124,6 +125,17 @@ class VisualGenerator: position=pos) break + def _generate_stub_planes(self): + mission_units = set() + for coalition_name, coalition in self.mission.coalition.items(): + for country in coalition.countries.values(): + for group in country.plane_group + country.helicopter_group + country.vehicle_group: + for unit in group.units: + mission_units.add(db.unit_type_of(unit)) + + for unit_type in mission_units: + self.mission.static_group(self.mission.country("USA"), "a", unit_type, Point(0, 300000), hidden=True) + def generate_target_smokes(self, target): spread = target.size * DESTINATION_SMOKE_DISTANCE_FACTOR for _ in range(0, int(target.size * DESTINATION_SMOKE_AMOUNT_FACTOR * (1.1 - target.base.strength))): @@ -159,3 +171,4 @@ class VisualGenerator: def generate(self): self._generate_frontline_smokes() + self._generate_stub_planes() diff --git a/pyinstaller.spec b/pyinstaller.spec new file mode 100644 index 00000000..dd61e5f3 --- /dev/null +++ b/pyinstaller.spec @@ -0,0 +1,40 @@ +# -*- mode: python -*- + +block_cipher = None + + +a = Analysis(['__init__.py'], + pathex=['C:\\Users\\shdwp\\PycharmProjects\\dcs_liberation'], + binaries=[], + datas=[ + ('resources', 'resources'), + ('submodules/dcs/dcs/terrain/caucasus.p', 'dcs/terrain/'), + ('submodules/dcs/dcs/terrain/nevada.p', 'dcs/terrain/'), + ], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + [], + icon="resources/icon.ico", + exclude_binaries=True, + name='liberation_main', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True ) +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + name='dcs_liberation') diff --git a/resources/caumap.gif b/resources/caumap.gif index 2bfe9259..6c974b47 100644 Binary files a/resources/caumap.gif and b/resources/caumap.gif differ diff --git a/icon.ico b/resources/icon.ico similarity index 100% rename from icon.ico rename to resources/icon.ico diff --git a/resources/nevada.gif b/resources/nevada.gif index c73532c5..d57b199e 100644 Binary files a/resources/nevada.gif and b/resources/nevada.gif differ diff --git a/resources/persiangulf.gif b/resources/persiangulf.gif index af08e704..88f74c1a 100644 Binary files a/resources/persiangulf.gif and b/resources/persiangulf.gif differ 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/tools/mkrelease.py b/resources/tools/mkrelease.py index 2295267b..4b89c24e 100644 --- a/resources/tools/mkrelease.py +++ b/resources/tools/mkrelease.py @@ -1,4 +1,5 @@ import os +import shutil from zipfile import * @@ -42,11 +43,13 @@ def _mk_archieve(): print("version already exists") return + shutil.rmtree("./dist") + + os.system("pyinstaller.exe pyinstaller.spec") + archieve = ZipFile(path, "w") - archieve.writestr("start.bat", "py.exe __init__.py \"%UserProfile%\\Saved Games\" \"{}\"".format(VERSION)) - _zip_dir(archieve, ".") - os.chdir("submodules\\dcs") - _zip_dir(archieve, "dcs") + archieve.writestr("dcs_liberation.bat", "cd dist\\dcs_liberation\r\nliberation_main \"%UserProfile%\\Saved Games\" \"{}\"".format(VERSION)) + _zip_dir(archieve, "./dist/dcs_liberation") _mk_archieve() \ No newline at end of file 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/resources/ui/ground_assets/aa.png b/resources/ui/ground_assets/aa.png new file mode 100644 index 00000000..26ea05c7 Binary files /dev/null and b/resources/ui/ground_assets/aa.png differ diff --git a/resources/ui/ground_assets/ammo.png b/resources/ui/ground_assets/ammo.png new file mode 100644 index 00000000..c2e6ffc0 Binary files /dev/null and b/resources/ui/ground_assets/ammo.png differ diff --git a/resources/ui/ground_assets/cleared.png b/resources/ui/ground_assets/cleared.png new file mode 100644 index 00000000..2912f6de Binary files /dev/null and b/resources/ui/ground_assets/cleared.png differ diff --git a/resources/ui/ground_assets/comms.png b/resources/ui/ground_assets/comms.png new file mode 100644 index 00000000..bff1a9f9 Binary files /dev/null and b/resources/ui/ground_assets/comms.png differ diff --git a/resources/ui/ground_assets/factory.png b/resources/ui/ground_assets/factory.png new file mode 100644 index 00000000..0d9f3b2d Binary files /dev/null and b/resources/ui/ground_assets/factory.png differ diff --git a/resources/ui/ground_assets/farp.png b/resources/ui/ground_assets/farp.png new file mode 100644 index 00000000..62fef986 Binary files /dev/null and b/resources/ui/ground_assets/farp.png differ diff --git a/resources/ui/ground_assets/fob.png b/resources/ui/ground_assets/fob.png new file mode 100644 index 00000000..27b0ab33 Binary files /dev/null and b/resources/ui/ground_assets/fob.png differ diff --git a/resources/ui/ground_assets/fuel.png b/resources/ui/ground_assets/fuel.png new file mode 100644 index 00000000..1324adcb Binary files /dev/null and b/resources/ui/ground_assets/fuel.png differ diff --git a/resources/ui/ground_assets/oil.png b/resources/ui/ground_assets/oil.png new file mode 100644 index 00000000..548cdbab Binary files /dev/null and b/resources/ui/ground_assets/oil.png differ diff --git a/resources/ui/ground_assets/power.png b/resources/ui/ground_assets/power.png new file mode 100644 index 00000000..fd0260d1 Binary files /dev/null and b/resources/ui/ground_assets/power.png differ diff --git a/resources/ui/ground_assets/target.png b/resources/ui/ground_assets/target.png new file mode 100644 index 00000000..c93255c1 Binary files /dev/null and b/resources/ui/ground_assets/target.png differ diff --git a/resources/ui/ground_assets/warehouse.png b/resources/ui/ground_assets/warehouse.png new file mode 100644 index 00000000..fa7a38e1 Binary files /dev/null and b/resources/ui/ground_assets/warehouse.png differ diff --git a/submodules/dcs b/submodules/dcs index 54eab60f..4fbb7ad3 160000 --- a/submodules/dcs +++ b/submodules/dcs @@ -1 +1 @@ -Subproject commit 54eab60f228847f2d92e344e6885eca855354f41 +Subproject commit 4fbb7ad3e0e2eecedc4e1dd14f2eb18025fef9f5 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..f84c5836 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,10 @@ +from tests.integration import baseattack, convoystrike, frontlineattack, insurgentattack, intercept, navalintercept, strike + +if __name__ == "__main__": + baseattack.execute_all() + convoystrike.execute_all() + frontlineattack.execute_all() + insurgentattack.execute_all() + intercept.execute_all() + navalintercept.execute_all() + strike.execute_all() diff --git a/tests/integration/baseattack.py b/tests/integration/baseattack.py new file mode 100644 index 00000000..0b867745 --- /dev/null +++ b/tests/integration/baseattack.py @@ -0,0 +1,46 @@ +from theater.caucasus import CaucasusTheater +from theater.nevada import NevadaTheater + +from tests.integration.util import * + +PLAYER_COUNTRY = "USA" +ENEMY_COUNTRY = "Russia" + + +def execute(game, player_cp, enemy_cp, departure_cp = None): + e = BaseAttackEvent(game, player_cp, enemy_cp, enemy_cp.position, PLAYER_COUNTRY, ENEMY_COUNTRY) + + departures = [departure_cp] if departure_cp else game.theater.player_points() + for departure_cp in departures: + if e.is_departure_available_from(departure_cp): + print("{} for {} ({}) - {}".format(e, player_cp, departure_cp, enemy_cp)) + e.departure_cp = departure_cp + e.player_attacking(autoflights_for(e, PLAYER_COUNTRY)) + + e.generate() + execute_autocommit(e) + e.generate_quick() + execute_autocommit(e) + + +def execute_theater(theater_klass): + print("Theater: {}".format(theater_klass)) + game, theater = init(PLAYER_COUNTRY, ENEMY_COUNTRY, theater_klass) + + total_events = 0 + while len(theater.enemy_points()) > 0: + for player_cp, enemy_cp in theater.conflicts(): + execute(game, player_cp, enemy_cp) + + enemy_cp.captured = True + + print("Total: {}".format(total_events)) + + +def execute_all(): + for theater_klass in [CaucasusTheater, PersianGulfTheater, NevadaTheater]: + execute_theater(theater_klass) + + +if __name__ == "__main__": + execute_all() diff --git a/tests/integration/convoystrike.py b/tests/integration/convoystrike.py new file mode 100644 index 00000000..d690a38c --- /dev/null +++ b/tests/integration/convoystrike.py @@ -0,0 +1,50 @@ +from theater.caucasus import CaucasusTheater +from theater.nevada import NevadaTheater + +from tests.integration.util import * + +PLAYER_COUNTRY = "USA" +ENEMY_COUNTRY = "Russia" + + +def execute(game, player_cp, enemy_cp, departure_cp = None): + e = ConvoyStrikeEvent(game, player_cp, enemy_cp, enemy_cp.position, PLAYER_COUNTRY, ENEMY_COUNTRY) + + departures = [departure_cp] if departure_cp else game.theater.player_points() + for departure_cp in departures: + if e.is_departure_available_from(departure_cp): + enemy_cp.base.strength = 1 + for _ in range(10): + print("{} for {} ({}) - {} ({})".format(e, player_cp, departure_cp, enemy_cp, enemy_cp.base.strength)) + e.departure_cp = departure_cp + e.player_attacking(autoflights_for(e, PLAYER_COUNTRY)) + + e.generate() + execute_autocommit(e) + e.generate_quick() + execute_autocommit(e) + + enemy_cp.base.affect_strength(-0.1) + + +def execute_theater(theater_klass): + print("Theater: {}".format(theater_klass)) + game, theater = init(PLAYER_COUNTRY, ENEMY_COUNTRY, theater_klass) + + total_events = 0 + while len(theater.enemy_points()) > 0: + for player_cp, enemy_cp in theater.conflicts(): + execute(game, player_cp, enemy_cp) + + enemy_cp.captured = True + + print("Total: {}".format(total_events)) + + +def execute_all(): + for theater_klass in [CaucasusTheater, PersianGulfTheater, NevadaTheater]: + execute_theater(theater_klass) + + +if __name__ == "__main__": + execute_all() diff --git a/tests/integration/frontlineattack.py b/tests/integration/frontlineattack.py new file mode 100644 index 00000000..695a76da --- /dev/null +++ b/tests/integration/frontlineattack.py @@ -0,0 +1,52 @@ +from theater.caucasus import CaucasusTheater +from theater.nevada import NevadaTheater + +from game.event.frontlineattack import FrontlineAttackEvent + +from tests.integration.util import * + +PLAYER_COUNTRY = "USA" +ENEMY_COUNTRY = "Russia" + + +def execute(game, player_cp, enemy_cp, departure_cp = None): + e = FrontlineAttackEvent(game, player_cp, enemy_cp, enemy_cp.position, PLAYER_COUNTRY, ENEMY_COUNTRY) + + departures = [departure_cp] if departure_cp else game.theater.player_points() + for departure_cp in departures: + if e.is_departure_available_from(departure_cp): + enemy_cp.base.strength = 1 + for _ in range(10): + print("{} for {} ({}) - {} ({})".format(e, player_cp, departure_cp, enemy_cp, enemy_cp.base.strength)) + e.departure_cp = departure_cp + e.player_attacking(autoflights_for(e, PLAYER_COUNTRY)) + + e.generate() + execute_autocommit(e) + e.generate_quick() + execute_autocommit(e) + + enemy_cp.base.affect_strength(-0.1) + + +def execute_theater(theater_klass): + print("Theater: {}".format(theater_klass)) + game, theater = init(PLAYER_COUNTRY, ENEMY_COUNTRY, theater_klass) + + total_events = 0 + while len(theater.enemy_points()) > 0: + for player_cp, enemy_cp in theater.conflicts(): + execute(game, player_cp, enemy_cp) + + enemy_cp.captured = True + + print("Total: {}".format(total_events)) + + +def execute_all(): + for theater_klass in [CaucasusTheater, PersianGulfTheater, NevadaTheater]: + execute_theater(theater_klass) + + +if __name__ == "__main__": + execute_all() diff --git a/tests/integration/insurgentattack.py b/tests/integration/insurgentattack.py new file mode 100644 index 00000000..b996e242 --- /dev/null +++ b/tests/integration/insurgentattack.py @@ -0,0 +1,48 @@ +from theater.caucasus import CaucasusTheater +from theater.nevada import NevadaTheater + +from game.event.insurgentattack import InsurgentAttackEvent + +from tests.integration.util import * + +PLAYER_COUNTRY = "USA" +ENEMY_COUNTRY = "Russia" + + +def execute(game, player_cp, enemy_cp, departure_cp = None): + e = InsurgentAttackEvent(game, enemy_cp, player_cp, player_cp.position, ENEMY_COUNTRY, PLAYER_COUNTRY) + + departures = [departure_cp] if departure_cp else game.theater.player_points() + for departure_cp in departures: + if e.is_departure_available_from(departure_cp): + print("{} for {} ({}) - {} ({})".format(e, player_cp, departure_cp, enemy_cp, enemy_cp.base.strength)) + e.departure_cp = departure_cp + e.player_defending(autoflights_for(e, PLAYER_COUNTRY)) + + e.generate() + execute_autocommit(e) + e.generate_quick() + execute_autocommit(e) + + +def execute_theater(theater_klass): + print("Theater: {}".format(theater_klass)) + game, theater = init(PLAYER_COUNTRY, ENEMY_COUNTRY, theater_klass) + + total_events = 0 + while len(theater.enemy_points()) > 0: + for player_cp, enemy_cp in theater.conflicts(): + execute(game, player_cp, enemy_cp) + + enemy_cp.captured = True + + print("Total: {}".format(total_events)) + + +def execute_all(): + for theater_klass in [CaucasusTheater, PersianGulfTheater, NevadaTheater]: + execute_theater(theater_klass) + + +if __name__ == "__main__": + execute_all() diff --git a/tests/integration/intercept.py b/tests/integration/intercept.py new file mode 100644 index 00000000..933c7a7c --- /dev/null +++ b/tests/integration/intercept.py @@ -0,0 +1,48 @@ +from theater.caucasus import CaucasusTheater +from theater.nevada import NevadaTheater + +from game.event.intercept import InterceptEvent + +from tests.integration.util import * + +PLAYER_COUNTRY = "USA" +ENEMY_COUNTRY = "Russia" + + +def execute(game, player_cp, enemy_cp, departure_cp = None): + e = InterceptEvent(game, player_cp, enemy_cp, enemy_cp.position, PLAYER_COUNTRY, ENEMY_COUNTRY) + + departures = [departure_cp] if departure_cp else game.theater.player_points() + for departure_cp in departures: + if e.is_departure_available_from(departure_cp): + print("{} for {} ({}) - {} ({})".format(e, player_cp, departure_cp, enemy_cp, enemy_cp.base.strength)) + e.departure_cp = departure_cp + e.player_attacking(autoflights_for(e, PLAYER_COUNTRY)) + + e.generate() + execute_autocommit(e) + e.generate_quick() + execute_autocommit(e) + + +def execute_theater(theater_klass): + print("Theater: {}".format(theater_klass)) + game, theater = init(PLAYER_COUNTRY, ENEMY_COUNTRY, theater_klass) + + total_events = 0 + while len(theater.enemy_points()) > 0: + for player_cp, enemy_cp in theater.conflicts(): + execute(game, player_cp, enemy_cp) + + enemy_cp.captured = True + + print("Total: {}".format(total_events)) + + +def execute_all(): + for theater_klass in [CaucasusTheater, PersianGulfTheater, NevadaTheater]: + execute_theater(theater_klass) + + +if __name__ == "__main__": + execute_all() diff --git a/tests/integration/navalintercept.py b/tests/integration/navalintercept.py new file mode 100644 index 00000000..e2aef1a9 --- /dev/null +++ b/tests/integration/navalintercept.py @@ -0,0 +1,49 @@ +from theater.caucasus import CaucasusTheater +from theater.nevada import NevadaTheater + +from game.event.intercept import InterceptEvent + +from tests.integration.util import * + +PLAYER_COUNTRY = "USA" +ENEMY_COUNTRY = "Russia" + + +def execute(game, player_cp, enemy_cp, departure_cp = None): + e = NavalInterceptEvent(game, player_cp, enemy_cp, enemy_cp.position, PLAYER_COUNTRY, ENEMY_COUNTRY) + + departures = [departure_cp] if departure_cp else game.theater.player_points() + for departure_cp in departures: + if e.is_departure_available_from(departure_cp): + print("{} for {} ({}) - {} ({})".format(e, player_cp, departure_cp, enemy_cp, enemy_cp.base.strength)) + e.departure_cp = departure_cp + e.player_attacking(autoflights_for(e, PLAYER_COUNTRY)) + + e.generate() + execute_autocommit(e) + e.generate_quick() + execute_autocommit(e) + + +def execute_theater(theater_klass): + print("Theater: {}".format(theater_klass)) + game, theater = init(PLAYER_COUNTRY, ENEMY_COUNTRY, theater_klass) + + total_events = 0 + while len(theater.enemy_points()) > 0: + for player_cp, enemy_cp in theater.conflicts(): + if enemy_cp.radials != LAND: + execute(game, player_cp, enemy_cp) + + enemy_cp.captured = True + + print("Total: {}".format(total_events)) + + +def execute_all(): + for theater_klass in [CaucasusTheater, PersianGulfTheater, NevadaTheater]: + execute_theater(theater_klass) + + +if __name__ == "__main__": + execute_all() diff --git a/tests/integration/strike.py b/tests/integration/strike.py new file mode 100644 index 00000000..7c6fa190 --- /dev/null +++ b/tests/integration/strike.py @@ -0,0 +1,48 @@ +from theater.caucasus import CaucasusTheater +from theater.nevada import NevadaTheater + +from game.event.intercept import InterceptEvent + +from tests.integration.util import * + +PLAYER_COUNTRY = "USA" +ENEMY_COUNTRY = "Russia" + + +def execute(game, player_cp, enemy_cp, departure_cp = None): + e = StrikeEvent(game, player_cp, enemy_cp, enemy_cp.position, PLAYER_COUNTRY, ENEMY_COUNTRY) + + departures = [departure_cp] if departure_cp else game.theater.player_points() + for departure_cp in departures: + if e.is_departure_available_from(departure_cp): + print("{} for {} ({}) - {} ({})".format(e, player_cp, departure_cp, enemy_cp, enemy_cp.base.strength)) + e.departure_cp = departure_cp + e.player_attacking(autoflights_for(e, PLAYER_COUNTRY)) + + e.generate() + execute_autocommit(e) + e.generate_quick() + execute_autocommit(e) + + +def execute_theater(theater_klass): + print("Theater: {}".format(theater_klass)) + game, theater = init(PLAYER_COUNTRY, ENEMY_COUNTRY, theater_klass) + + total_events = 0 + while len(theater.enemy_points()) > 0: + for player_cp, enemy_cp in theater.conflicts(): + execute(game, player_cp, enemy_cp) + + enemy_cp.captured = True + + print("Total: {}".format(total_events)) + + +def execute_all(): + for theater_klass in [CaucasusTheater, PersianGulfTheater, NevadaTheater]: + execute_theater(theater_klass) + + +if __name__ == "__main__": + execute_all() diff --git a/tests/integration/util.py b/tests/integration/util.py new file mode 100644 index 00000000..46310e43 --- /dev/null +++ b/tests/integration/util.py @@ -0,0 +1,80 @@ +from dcs.mission import Mission + +from game import * +from game.event import * +from game.db import * + +from theater.persiangulf import * +from theater import start_generator + +PLAYER_COUNTRY = None +ENEMY_COUNTRY = None + + +def init(player_country: str, enemy_country: str, theater_klass: typing.Type[ConflictTheater]) -> typing.Tuple[Game, ConflictTheater]: + global PLAYER_COUNTRY + global ENEMY_COUNTRY + + PLAYER_COUNTRY = player_country + ENEMY_COUNTRY = enemy_country + + # prerequisites + persistency.setup("./tests/userfolder/") + theater = theater_klass() + start_generator.generate_inital_units(theater, ENEMY_COUNTRY, True, 1) + start_generator.generate_groundobjects(theater) + return Game(PLAYER_COUNTRY, ENEMY_COUNTRY, theater), theater + + +def autoflights_for(event: Event, country: str) -> TaskForceDict: + result = {} + for task in event.tasks: + result[task] = {find_unittype(task, country)[0]: (1, 1)} + return result + + +class AutodebriefType(Enum): + EVERYONE_DEAD = 0 + PLAYER_DEAD = 1 + ENEMY_DEAD = 2 + + +def autodebrief_for(event: Event, type: AutodebriefType) -> Debriefing: + mission = event.operation.current_mission # type: Mission + + countries = [] + if type == AutodebriefType.PLAYER_DEAD or type == AutodebriefType.EVERYONE_DEAD: + countries.append(mission.country(PLAYER_COUNTRY)) + + if type == AutodebriefType.ENEMY_DEAD or type == AutodebriefType.EVERYONE_DEAD: + countries.append(mission.country(ENEMY_COUNTRY)) + + dead_units = [] + for country in countries: + for group in country.plane_group + country.vehicle_group + country.helicopter_group: + for unit in group.units: + dead_units.append(str(unit.name)) + + return Debriefing(dead_units, []) + + +def event_state_save(e: Event) -> typing.Tuple[Base, Base]: + return (copy.deepcopy(e.from_cp.base), copy.deepcopy(e.to_cp.base)) + + +def event_state_restore(e: Event, state: typing.Tuple[Base, Base]): + e.from_cp.base, e.to_cp.base = state[0], state[1] + + +def execute_autocommit(e: Event): + state = event_state_save(e) + e.commit(autodebrief_for(e, AutodebriefType.EVERYONE_DEAD)) + event_state_restore(e, state) + + state = event_state_save(e) + e.commit(autodebrief_for(e, AutodebriefType.PLAYER_DEAD)) + event_state_restore(e, state) + + state = event_state_save(e) + e.commit(autodebrief_for(e, AutodebriefType.ENEMY_DEAD)) + event_state_restore(e, state) diff --git a/tests/userfolder/DCS/Missions/empty.txt b/tests/userfolder/DCS/Missions/empty.txt new file mode 100644 index 00000000..e69de29b diff --git a/theater/base.py b/theater/base.py index c62c1474..5f3b99cb 100644 --- a/theater/base.py +++ b/theater/base.py @@ -56,7 +56,7 @@ class Base: def _find_best_unit(self, dict, for_type: Task, count: int) -> typing.Dict: if count <= 0: - logging.info("{}: no units for {}".format(self, for_type)) + logging.warning("{}: no units for {}".format(self, for_type)) return {} sorted_units = [key for key in dict.keys() if key in db.UNIT_BY_TASK[for_type]] diff --git a/theater/caucasus.py b/theater/caucasus.py index b6506b0b..b4256a99 100644 --- a/theater/caucasus.py +++ b/theater/caucasus.py @@ -11,8 +11,8 @@ from .base import * class CaucasusTheater(ConflictTheater): terrain = caucasus.Caucasus() overview_image = "caumap.gif" - reference_points = {(-317948.32727306, 635639.37385346): (278.5, 319), - (-355692.3067714, 617269.96285781): (263, 352), } + reference_points = {(-317948.32727306, 635639.37385346): (278.5*2, 319*2), + (-355692.3067714, 617269.96285781): (263*2, 352*2), } landmap = load_landmap("resources\\caulandmap.p") daytime_map = { "dawn": (6, 9), diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index 9d7a634a..9dd258ea 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -18,8 +18,6 @@ IMPORTANCE_LOW = 1 IMPORTANCE_MEDIUM = 1.2 IMPORTANCE_HIGH = 1.4 -GLOBAL_CP_CONFLICT_DISTANCE_MIN = 340000 - """ ALL_RADIALS = [0, 45, 90, 135, 180, 225, 270, 315, ] COAST_NS_E = [45, 90, 135, ] @@ -55,10 +53,18 @@ class ConflictTheater: reference_points = None # type: typing.Dict overview_image = None # type: str landmap = None # type: landmap.Landmap + """ + land_poly = None # type: Polygon + """ daytime_map = None # type: typing.Dict[str, typing.Tuple[int, int]] def __init__(self): self.controlpoints = [] + """ + self.land_poly = geometry.Polygon(self.landmap[0][0]) + for x in self.landmap[1]: + self.land_poly = self.land_poly.difference(geometry.Polygon(x)) + """ def add_controlpoint(self, point: ControlPoint, connected_to: typing.Collection[ControlPoint] = []): for connected_point in connected_to: @@ -102,9 +108,5 @@ class ConflictTheater: for connected_point in [x for x in cp.connected_points if x.captured != from_player]: yield (cp, connected_point) - for global_cp in [x for x in self.controlpoints if x.is_global and x.captured == from_player]: - if global_cp.position.distance_to_point(connected_point.position) < GLOBAL_CP_CONFLICT_DISTANCE_MIN: - yield (global_cp, connected_point) - def enemy_points(self) -> typing.Collection[ControlPoint]: return [point for point in self.controlpoints if not point.captured] diff --git a/theater/nevada.py b/theater/nevada.py index 66b8b350..54ede6ac 100644 --- a/theater/nevada.py +++ b/theater/nevada.py @@ -9,8 +9,8 @@ from .base import * class NevadaTheater(ConflictTheater): terrain = dcs.terrain.Nevada() overview_image = "nevada.gif" - reference_points = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45, -360), - (nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440, 80), } + reference_points = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45*2, -360*2), + (nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440*2, 80*2), } landmap = load_landmap("resources\\nev_landmap.p") daytime_map = { "dawn": (4, 6), @@ -19,7 +19,6 @@ class NevadaTheater(ConflictTheater): "night": (0, 5), } - mina = ControlPoint.from_airport(nevada.Mina_Airport_3Q0, LAND, SIZE_SMALL, IMPORTANCE_LOW) tonopah = ControlPoint.from_airport(nevada.Tonopah_Airport, LAND, SIZE_SMALL, IMPORTANCE_LOW) tonopah_test_range = ControlPoint.from_airport(nevada.Tonopah_Test_Range_Airfield, LAND, SIZE_SMALL, IMPORTANCE_LOW) lincoln_conty = ControlPoint.from_airport(nevada.Lincoln_County, LAND, SIZE_SMALL, 1.2) @@ -37,8 +36,7 @@ class NevadaTheater(ConflictTheater): def __init__(self): super(NevadaTheater, self).__init__() - self.add_controlpoint(self.mina, connected_to=[self.tonopah]) - self.add_controlpoint(self.tonopah, connected_to=[self.mina, self.tonopah_test_range, self.lincoln_conty]) + self.add_controlpoint(self.tonopah, connected_to=[self.tonopah_test_range, self.lincoln_conty]) self.add_controlpoint(self.tonopah_test_range, connected_to=[self.tonopah, self.lincoln_conty, self.groom_lake, self.pahute_mesa]) self.add_controlpoint(self.lincoln_conty, connected_to=[self.tonopah_test_range, self.mesquite]) @@ -52,5 +50,5 @@ class NevadaTheater(ConflictTheater): self.add_controlpoint(self.jean, connected_to=[self.laughlin, self.las_vegas]) self.add_controlpoint(self.laughlin, connected_to=[self.jean, self.las_vegas]) - self.mina.captured = True + self.tonopah.captured = True diff --git a/theater/persiangulf.py b/theater/persiangulf.py index ec55045f..6f4b0397 100644 --- a/theater/persiangulf.py +++ b/theater/persiangulf.py @@ -9,8 +9,8 @@ from .landmap import load_landmap class PersianGulfTheater(ConflictTheater): terrain = dcs.terrain.PersianGulf() overview_image = "persiangulf.gif" - reference_points = {(persiangulf.Sir_Abu_Nuayr.position.x, persiangulf.Sir_Abu_Nuayr.position.y): (321, 145), - (persiangulf.Sirri_Island.position.x, persiangulf.Sirri_Island.position.y): (347, 82), } + reference_points = {(persiangulf.Sir_Abu_Nuayr.position.x, persiangulf.Sir_Abu_Nuayr.position.y): (321*4, 145*4), + (persiangulf.Sirri_Island.position.x, persiangulf.Sirri_Island.position.y): (347*4, 82*4), } landmap = load_landmap("resources\\gulflandmap.p") daytime_map = { "dawn": (6, 8), diff --git a/theater/start_generator.py b/theater/start_generator.py index 2c4d9c87..6a95f705 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -72,10 +72,20 @@ def generate_groundobjects(theater: ConflictTheater): return None group_id = 0 - for cp in theater.enemy_points(): - for _ in range(0, random.randrange(2, 4)): - available_categories = list(tpls) + ["aa", "aa"] - tpl_category = random.choice(available_categories) + for cp in theater.controlpoints: + if cp.is_global: + continue + + if not cp.has_frontline: + continue + + amount = random.randrange(5, 7) + for i in range(0, amount): + available_categories = list(tpls) + if i >= amount - 1: + tpl_category = "aa" + else: + tpl_category = random.choice(available_categories) tpl = random.choice(list(tpls[tpl_category].values())) @@ -85,13 +95,6 @@ def generate_groundobjects(theater: ConflictTheater): print("Couldn't find point for {}".format(cp)) continue - """ - dist = point.distance_to_point(cp.position) - 15000 - for another_cp in theater.enemy_points(): - if another_cp.position.distance_to_point(point) < dist: - cp = another_cp - """ - group_id += 1 object_id = 0 diff --git a/ui/configurationmenu.py b/ui/configurationmenu.py index f43fdc36..84b552d3 100644 --- a/ui/configurationmenu.py +++ b/ui/configurationmenu.py @@ -21,6 +21,12 @@ class ConfigurationMenu(Menu): self.enemy_vehicle_var = StringVar() self.enemy_vehicle_var.set(self.game.settings.enemy_vehicle_skill) + self.map_coalition_visibility_var = StringVar() + self.map_coalition_visibility_var.set(self.game.settings.map_coalition_visibility) + + self.labels_var = StringVar() + self.labels_var.set(self.game.settings.labels) + self.takeoff_var = BooleanVar() self.takeoff_var.set(self.game.settings.only_player_takeoff) @@ -34,6 +40,8 @@ class ConfigurationMenu(Menu): self.game.settings.player_skill = self.player_skill_var.get() self.game.settings.enemy_skill = self.enemy_skill_var.get() self.game.settings.enemy_vehicle_skill = self.enemy_vehicle_var.get() + self.game.settings.map_coalition_visibility = self.map_coalition_visibility_var.get() + self.game.settings.labels = self.labels_var.get() self.game.settings.only_player_takeoff = self.takeoff_var.get() self.game.settings.night_disabled = self.night_var.get() self.game.settings.cold_start = self.cold_start_var.get() @@ -72,6 +80,18 @@ class ConfigurationMenu(Menu): e_skill.configure(**STYLES["btn-primary"]) row += 1 + Label(body, text="F10 Map Coalition Visibility", **STYLES["widget"]).grid(row=row, column=0, sticky=W) + map_vis = OptionMenu(body, self.map_coalition_visibility_var, "All Units", "Allied Units", "Own Aircraft", "None") + map_vis.grid(row=row, column=1, sticky=E) + map_vis.configure(**STYLES["btn-primary"]) + row += 1 + + Label(body, text="In Game Labels", **STYLES["widget"]).grid(row=row, column=0, sticky=W) + g_labels = OptionMenu(body, self.labels_var, "Full", "Abbreviated", "Dot Only", "Off") + g_labels.grid(row=row, column=1, sticky=E) + g_labels.configure(**STYLES["btn-primary"]) + row += 1 + Label(body, text="Aircraft cold start", **STYLES["widget"]).grid(row=row, column=0, sticky=W) Checkbutton(body, variable=self.cold_start_var, **STYLES["radiobutton"]).grid(row=row, column=1, sticky=E) row += 1 @@ -84,9 +104,6 @@ class ConfigurationMenu(Menu): Checkbutton(body, variable=self.night_var, **STYLES["radiobutton"]).grid(row=row, column=1, sticky=E) row += 1 - Button(body, text="Display logs", command=self.display_logs, **STYLES["btn-primary"]).grid(row=row, column=1, sticky=E, pady=30) - row += 1 - Label(body, text="Contributors: ", **STYLES["strong"]).grid(row=row, column=0, columnspan=2, sticky=EW) row += 1 @@ -98,7 +115,8 @@ class ConfigurationMenu(Menu): Button(body, text="[github]", command=lambda: webbrowser.open_new_tab("http://github.com/Khopa"), **STYLES["widget"]).grid(row=row, column=1, sticky=E) row += 1 - Button(body, text="Cheat +200m", command=self.cheat_money, **STYLES["btn-danger"]).grid(row=row, column=1, pady=30) + Button(body, text="Display logs", command=self.display_logs, **STYLES["btn-primary"]).grid(row=row, column=0, pady=5) + Button(body, text="Cheat +200m", command=self.cheat_money, **STYLES["btn-danger"]).grid(row=row, column=1) def display_logs(self): raise ShowLogsException() diff --git a/ui/eventmenu.py b/ui/eventmenu.py index 64b9c580..24aa7e70 100644 --- a/ui/eventmenu.py +++ b/ui/eventmenu.py @@ -20,7 +20,7 @@ class EventMenu(Menu): self.scramble_entries = {k: {} for k in self.event.tasks} if self.event.attacker_name == self.game.player: - self.base = self.event.from_cp.base + self.base = self.event.departure_cp.base else: self.base = self.event.to_cp.base @@ -194,15 +194,24 @@ class EventMenu(Menu): self.error_label["text"] = "Need at least one player in flight {}".format(self.event.flight_name(task)) return - if isinstance(self.event, FrontlineAttackEvent) or isinstance(self.event, FrontlinePatrolEvent): - if tasks_scramble_counts.get(PinpointStrike, 0) == 0: - self.error_label["text"] = "No ground vehicles assigned to attack!" + for task in self.event.player_banned_tasks: + if tasks_clients_counts.get(task, 0) != 0: + self.error_label["text"] = "Players are not allowed on flight {}".format(self.event.flight_name(task)) return - if self.game.is_player_attack(self.event): + if isinstance(self.event, FrontlineAttackEvent) or isinstance(self.event, FrontlinePatrolEvent): + if self.event.from_cp.base.total_armor == 0: + self.error_label["text"] = "No ground vehicles available to attack!" + return + self.event.player_attacking(flights) else: + if isinstance(self.event, FrontlineAttackEvent) or isinstance(self.event, FrontlinePatrolEvent): + if self.event.to_cp.base.total_armor == 0: + self.error_label["text"] = "No ground vehicles available to defend!" + return + self.event.player_defending(flights) self.game.initiate_event(self.event) diff --git a/ui/mainmenu.py b/ui/mainmenu.py index 9117e9b6..79f95cfb 100644 --- a/ui/mainmenu.py +++ b/ui/mainmenu.py @@ -25,85 +25,11 @@ class MainMenu(Menu): def display(self): persistency.save_game(self.game) - self.window.clear_right_pane() self.upd.update() - # Header : header = Frame(self.frame, **STYLES["header"]) - Button(header, text="Configuration", command=self.configuration_menu, **STYLES["btn-primary"]).grid(column=0, row=0, sticky=NW) - Label(header, text="Budget: {}m (+{}m)".format(self.game.budget, self.game.budget_reward_amount), **STYLES["strong"]).grid(column=1, row=0, sticky=N+EW, padx=50) - Button(header, text="Pass turn", command=self.pass_turn, **STYLES["btn-primary"]).grid(column=2, row=0, sticky=NE) - header.grid(column=0, row=0, sticky=N+EW) - - content = Frame(self.frame, **STYLES["body"]) - content.grid(column=0, row=1, sticky=NSEW) - column = 0 - row = 0 - - def label(text): - nonlocal row, body - frame = LabelFrame(body, **STYLES["label-frame"]) - frame.grid(row=row, sticky=N+EW, columnspan=2) - Label(frame, text=text, **STYLES["widget"]).grid(row=row, sticky=NS) - row += 1 - - def event_button(event): - nonlocal row, body - frame = LabelFrame(body, **STYLES["label-frame"]) - frame.grid(row=row, sticky=N+EW) - Message(frame, text="{}".format( - event - ), aspect=1600, **STYLES["widget"]).grid(column=0, row=0, sticky=N+EW) - Button(body, text=">", command=self.start_event(event), **STYLES["btn-primary"]).grid(column=1, row=row, sticky=E) - row += 1 - - def departure_header(text, style="strong"): - nonlocal row, body - Label(body, text=text, **STYLES[style]).grid(column=0, columnspan=2, row=row, sticky=N+EW, pady=(0, 5)) - row += 1 - - def destination_header(text): - nonlocal row, body - Label(body, text=text, **STYLES["substrong"]).grid(column=0, columnspan=2, row=row, sticky=N+EW) - row += 1 - - events = self.game.events - events.sort(key=lambda x: x.to_cp.name) - events.sort(key=lambda x: x.from_cp.name) - events.sort(key=lambda x: x.informational and 1 or (self.game.is_player_attack(x) and 2 or 0)) - - destination = None - departure = None - - for event in events: - if event.informational: - new_departure = "Deliveries" - elif not self.game.is_player_attack(event): - new_departure = "Enemy attack" - else: - new_departure = event.from_cp.name - - if new_departure != departure: - body = Frame(content, **STYLES["body"]) - body.grid(column=column, row=1, sticky=N+EW) - row = 0 - column += 1 - - departure = new_departure - departure_header(new_departure, style="strong" if self.game.is_player_attack(event) else "supstrong") - destination = None - - if not event.informational: - new_destination = "At {}".format(event.to_cp.name) - if destination != new_destination: - destination_header(new_destination) - destination = new_destination - - if event.informational: - label(str(event)) - else: - event_button(event) + header.grid(column=0, row=0, sticky=NSEW) def pass_turn(self): self.game.pass_turn(no_action=True) @@ -113,7 +39,7 @@ class MainMenu(Menu): ConfigurationMenu(self.window, self, self.game).display() def start_event(self, event) -> typing.Callable: - return lambda: EventMenu(self.window, self, self.game, event).display() + EventMenu(self.window, self, self.game, event).display() def go_cp(self, cp: ControlPoint): if not cp.captured: diff --git a/ui/overviewcanvas.py b/ui/overviewcanvas.py index 23daec81..5b12e9cd 100644 --- a/ui/overviewcanvas.py +++ b/ui/overviewcanvas.py @@ -1,27 +1,536 @@ import os - -from tkinter import * +import platform +from threading import Thread from tkinter.ttk import * +import pygame + +from theater.theatergroundobject import CATEGORY_MAP +from ui.styles import STYLES from ui.window import * -from game.game import * -from gen.conflictgen import Conflict -from theater.conflicttheater import * + +EVENT_COLOR_ATTACK = (100, 100, 255) +EVENT_COLOR_DEFENSE = (255, 100, 100) + +RED = (255, 125, 125) +BRIGHT_RED = (200, 64, 64) +BLUE = (164, 164, 255) +DARK_BLUE = (45, 62, 80) +WHITE = (255, 255, 255) +GREEN = (128, 186, 128) +BRIGHT_GREEN = (64, 200, 64) +BLACK = (0, 0, 0) + +BACKGROUND = pygame.Color(0, 64, 64) +ANTIALIASING = True + +WIDTH = 800 +HEIGHT = 600 +MAP_PADDING = 100 class OverviewCanvas: mainmenu = None # type: ui.mainmenu.MainMenu + budget_label = None # type: Label + + started = None + ground_assets_icons = None # type: typing.Dict[str, pygame.Surface] + event_icons = None # type: typing.Dict[typing.Type, pygame.Surface] + selected_event_info = None # type: typing.Tuple[Event, typing.Tuple[int, int]] + frontline_vector_cache = None # type: typing.Dict[str, typing.Tuple[Point, int, int]] def __init__(self, frame: Frame, parent, game: Game): + self.parent = parent self.game = game - self.image = PhotoImage(file=os.path.join("resources", game.theater.overview_image)) - self.canvas = Canvas(frame, width=self.image.width(), height=self.image.height()) - self.canvas.grid(column=0, row=0, sticky=NSEW) + # Remove any previously existing pygame instance + pygame.quit() - def transform_point(self, p: Point, treshold=30) -> (int, int): + # Pygame objects + self.map = None + self.screen = None + self.surface: pygame.Surface = None + self.thread: Thread = None + self.clock = pygame.time.Clock() + self.expanded = True + + pygame.font.init() + self.font: pygame.font.SysFont = pygame.font.SysFont("arial", 15) + self.fontsmall: pygame.font.SysFont = pygame.font.SysFont("arial", 10) + self.ground_assets_icons = {} + + # Frontline are too heavy on performance to compute in realtime, so keep them in a cache + self.frontline_vector_cache = {} + + # Map state + self.redraw_required = True + self.zoom = 1 + self.scroll = [0, 0] + self.exited = False + + # Display options + self.display_ground_targets = BooleanVar(value=True) + self.display_forces = BooleanVar(value=True) + self.display_bases = BooleanVar(value=True) + self.display_road = BooleanVar(value=True) + self.display_rules = self.compute_display_rules() + + parent.window.tk.protocol("", self.on_close) + + self.wrapper = Frame(frame, **STYLES["frame-wrapper"]) + self.wrapper.grid(column=0, row=0, sticky=NSEW) # Adds grid + self.wrapper.pack(side=LEFT) # packs window to the left + + self.embed = Frame(self.wrapper, width=WIDTH, height=HEIGHT, borderwidth=2, **STYLES["frame-wrapper"]) + self.embed.grid(column=0, row=1, sticky=NSEW) # Adds grid + + self.options = Frame(self.wrapper, borderwidth=2, **STYLES["frame-wrapper"]) + self.options.grid(column=0, row=0, sticky=NSEW) + self.options.grid_columnconfigure(1, weight=1) + self.build_map_options_panel() + + self.init_sdl_layer() + self.init_sdl_thread() + + def build_map_options_panel(self): + col = 0 + Button(self.options, text="Configuration", command=self.parent.configuration_menu, **STYLES["btn-primary"]).grid(column=col, row=0, sticky=NE) + col += 1 + + self.budget_label = Label(self.options, text="Budget: {}m (+{}m)".format(self.game.budget, self.game.budget_reward_amount), **STYLES["widget"]) + self.budget_label.grid(column=col, row=0, sticky=N+EW) + col += 1 + + Button(self.options, text="Pass turn", command=self.parent.pass_turn, **STYLES["btn-primary"]).grid(column=col, row=0, sticky=NW) + col += 1 + + def map_size_toggle(self): + if self.expanded: + self.embed.configure(width=0) + self.options.configure(width=0) + self.expanded = False + else: + self.embed.configure(width=WIDTH) + self.options.configure(width=WIDTH) + self.expanded = True + + def on_close(self): + self.exited = True + if self.thread is not None: + self.thread.join() + + def init_sdl_layer(self): + # Setup pygame to run in tk frame + os.environ['SDL_WINDOWID'] = str(self.embed.winfo_id()) + if platform.system == "Windows": + os.environ['SDL_VIDEODRIVER'] = 'windib' + + # Create pygame 'screen' + self.screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.DOUBLEBUF | pygame.HWSURFACE) + self.screen.fill(pygame.Color(*BLACK)) + + # Load icons resources + self.ground_assets_icons = {} + self.ground_assets_icons["target"] = pygame.image.load(os.path.join("resources", "ui", "ground_assets", "target.png")) + self.ground_assets_icons["cleared"] = pygame.image.load(os.path.join("resources", "ui", "ground_assets", "cleared.png")) + for category in CATEGORY_MAP.keys(): + self.ground_assets_icons[category] = pygame.image.load(os.path.join("resources", "ui", "ground_assets", category + ".png")) + + self.event_icons = {} + for category, image in {BaseAttackEvent: "capture", + FrontlinePatrolEvent: "attack", + FrontlineAttackEvent: "attack", + InfantryTransportEvent: "infantry", + InsurgentAttackEvent: "insurgent_attack", + ConvoyStrikeEvent: "convoy", + InterceptEvent: "air_intercept", + NavalInterceptEvent: "naval_intercept", + StrikeEvent: "strike", + UnitsDeliveryEvent: "delivery"}.items(): + self.event_icons[category] = pygame.image.load(os.path.join("resources", "ui", "events", image + ".png")) + + + # Load the map image + self.map = pygame.image.load(os.path.join("resources", self.game.theater.overview_image)).convert() + pygame.draw.rect(self.map, BLACK, (0, 0, self.map.get_width(), self.map.get_height()), 10) + pygame.draw.rect(self.map, WHITE, (0, 0, self.map.get_width(), self.map.get_height()), 5) + + # Create surfaces for drawing + self.surface = pygame.Surface((self.map.get_width() + MAP_PADDING * 2, + self.map.get_height() + MAP_PADDING * 2)) + self.surface.set_alpha(None) + self.overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) + + # Init pygame display + pygame.display.init() + pygame.display.update() + + def init_sdl_thread(self): + if OverviewCanvas.started is not None: + OverviewCanvas.started.exited = True + self.thread = Thread(target=self.sdl_thread) + self.thread.start() + OverviewCanvas.started = self + print("Started SDL app") + + def sdl_thread(self): + self.redraw_required = True + i = 0 + while not self.exited: + self.clock.tick(30) + self.draw() + i += 1 + if i == 600: + self.frontline_vector_cache = {} + i = 0 + print("Stopped SDL app") + + def draw(self): + try: + self.embed.winfo_ismapped() + self.embed.winfo_manager() + except: + self.exited = True + + right_down = False + left_down = False + + # Detect changes on display rules + r = self.compute_display_rules() + if r != self.display_rules: + self.display_rules = r + self.redraw_required = True + + for event in pygame.event.get(): + if event.type == pygame.MOUSEMOTION: + self.redraw_required = True + elif event.type == pygame.MOUSEBUTTONDOWN: + """ + Due to rendering not really supporting the zoom this is currently disabled. + @TODO: improve rendering so zoom would actually make sense + + # Scroll wheel + if event.button == 4: + self.zoom += 0.25 + self.redraw_required = True + elif event.button == 5: + self.zoom -= 0.25 + self.redraw_required = True + """ + + if event.button == 3: + right_down = True + pygame.mouse.get_rel() + if event.button == 1: + left_down = True + self.redraw_required = True + + # If Right click pressed + if pygame.mouse.get_pressed()[2] == 1 and not right_down: + scr = pygame.mouse.get_rel() + self.scroll[0] += scr[0] + self.scroll[1] += scr[1] + self.redraw_required = True + + if self.zoom <= 0.5: + self.zoom = 0.5 + elif self.zoom > 3: + self.zoom = 3 + + if self.redraw_required: + # Fill + self.screen.fill(BACKGROUND) + self.surface.fill(BACKGROUND) + self.overlay.fill(pygame.Color(0, 0, 0, 0)) + + # Surface + cursor_pos = pygame.mouse.get_pos() + cursor_pos = ( + cursor_pos[0] / self.zoom - self.scroll[0], cursor_pos[1] / self.zoom - self.scroll[1]) + self.draw_map(self.surface, self.overlay, cursor_pos, [left_down, right_down]) + + # Scaling + scaled = pygame.transform.scale(self.surface, ( + int(self.surface.get_width() * self.zoom), int(self.surface.get_height() * self.zoom))) + self.screen.blit(scaled, (self.scroll[0]*self.zoom, self.scroll[1]*self.zoom)) + self.screen.blit(self.overlay, (0, 0)) + + pygame.display.flip() + + self.redraw_required = False + + def draw_map(self, surface: pygame.Surface, overlay: pygame.Surface, mouse_pos: (int, int), mouse_down: [bool, bool]): + self.surface.blit(self.map, (MAP_PADDING, MAP_PADDING)) + + # Display zoom level on overlay + zoom_lvl = self.font.render(" x " + str(self.zoom) + " ", ANTIALIASING, WHITE, DARK_BLUE) + self.overlay.blit(zoom_lvl, (self.overlay.get_width()-zoom_lvl.get_width()-5, + self.overlay.get_height()-zoom_lvl.get_height()-5)) + + # Debug + # pygame.draw.rect(surface, (255, 0, 255), (mouse_pos[0], mouse_pos[1], 5, 5), 2) + + for cp in self.game.theater.controlpoints: + coords = self._transform_point(cp.position) + + if self.display_road.get(): + for connected_cp in cp.connected_points: + connected_coords = self._transform_point(connected_cp.position) + if connected_cp.captured != cp.captured: + color = self._enemy_color() + elif connected_cp.captured and cp.captured: + color = self._player_color() + else: + color = BLACK + + pygame.draw.line(surface, color, coords, connected_coords, 2) + + if cp.captured and not connected_cp.captured and Conflict.has_frontline_between(cp, connected_cp): + frontline = self._frontline_vector(cp, connected_cp) + if not frontline: + continue + + frontline_pos, heading, distance = frontline + + if distance < 10000: + frontline_pos = frontline_pos.point_from_heading(heading + 180, 5000) + distance = 10000 + + start_coords = self._transform_point(frontline_pos, treshold=10) + end_coords = self._transform_point(frontline_pos.point_from_heading(heading, distance), + treshold=60) + + pygame.draw.line(surface, color, start_coords, end_coords, 4) + + if self.display_ground_targets.get(): + for ground_object in cp.ground_objects: + self.draw_ground_object(ground_object, surface, cp.captured, mouse_pos) + + if self.display_bases.get(): + mouse_down = self.draw_bases(mouse_pos, mouse_down) + + mouse_down = self.draw_events(self.surface, mouse_pos, mouse_down) + + if mouse_down[0]: + self.selected_event_info = None + + def draw_bases(self, mouse_pos, mouse_down): + for cp in self.game.theater.controlpoints: + coords = self._transform_point(cp.position) + radius = 12 * math.pow(cp.importance, 1) + radius_m = max(radius * cp.base.strength - 2, 0) + + if cp.captured: + color = self._player_color() + else: + color = self._enemy_color() + + pygame.draw.circle(self.surface, BLACK, (int(coords[0]), int(coords[1])), int(radius)) + pygame.draw.circle(self.surface, color, (int(coords[0]), int(coords[1])), int(radius_m)) + + label = self.font.render(cp.name, ANTIALIASING, (225, 225, 225), BLACK) + labelHover = self.font.render(cp.name, ANTIALIASING, (255, 255, 255), (128, 186, 128)) + labelClick = self.font.render(cp.name, ANTIALIASING, (255, 255, 255), (122, 122, 255)) + + point = coords[0] - label.get_width() / 2 + 1, coords[1] + 1 + rect = pygame.Rect(*point, label.get_width(), label.get_height()) + + if rect.collidepoint(*mouse_pos): + if mouse_down[0]: + self.surface.blit(labelClick, (coords[0] - label.get_width() / 2 + 1, coords[1] + 1)) + self._selected_cp(cp) + mouse_down[0] = False + else: + self.surface.blit(labelHover, (coords[0] - label.get_width() / 2 + 1, coords[1] + 1)) + + self.draw_base_info(self.overlay, cp, (0, 0)) + if self.selected_event_info: + if self._cp_available_for_selected_event(cp): + pygame.draw.line(self.surface, WHITE, rect.center, self.selected_event_info[1]) + + else: + self.surface.blit(label, (coords[0] - label.get_width() / 2 + 1, coords[1] + 1)) + + if self.display_forces.get(): + units_title = " {} / {} / {} ".format(cp.base.total_planes, cp.base.total_armor, cp.base.total_aa) + label2 = self.fontsmall.render(units_title, ANTIALIASING, color, (30, 30, 30)) + self.surface.blit(label2, (coords[0] - label2.get_width() / 2, coords[1] + label.get_height() + 1)) + + return mouse_down + + def draw_base_info(self, surface: pygame.Surface, control_point: ControlPoint, pos): + title = self.font.render(control_point.name, ANTIALIASING, BLACK, GREEN) + hp = self.font.render("Strength : ", ANTIALIASING, (225, 225, 225), BLACK) + + armor_txt = "ARMOR > " + for key, value in control_point.base.armor.items(): + armor_txt += key.id + " x " + str(value) + " | " + armor = self.font.render(armor_txt, ANTIALIASING, (225, 225, 225), BLACK) + + aircraft_txt = "AIRCRAFT > " + for key, value in control_point.base.aircraft.items(): + aircraft_txt += key.id + " x " + str(value) + " | " + aircraft = self.font.render(aircraft_txt, ANTIALIASING, (225, 225, 225), BLACK) + + aa_txt = "AA/SAM > " + for key, value in control_point.base.aa.items(): + aa_txt += key.id + " x " + str(value) + " | " + aa = self.font.render(aa_txt, ANTIALIASING, (225, 225, 225), BLACK) + + lineheight = title.get_height() + w = max([max([a.get_width() for a in [title, armor, aircraft, aa]]), 150]) + h = 5 * lineheight + 4 * 5 + + # Draw frame + pygame.draw.rect(surface, GREEN, (pos[0], pos[1], w + 8, h + 8)) + pygame.draw.rect(surface, BLACK, (pos[0] + 2, pos[1] + 2, w + 4, h + 4)) + pygame.draw.rect(surface, GREEN, (pos[0] + 2, pos[1], w + 4, lineheight + 4)) + + # Title + surface.blit(title, (pos[0] + 4, 4 + pos[1])) + surface.blit(hp, (pos[0] + 4, 4 + pos[1] + lineheight + 5)) + + # Draw gauge + pygame.draw.rect(surface, WHITE, + (pos[0] + hp.get_width() + 3, 4 + pos[1] + lineheight + 5, 54, lineheight)) + pygame.draw.rect(surface, BRIGHT_RED, + (pos[0] + hp.get_width() + 5, 4 + pos[1] + lineheight + 5 + 2, 50, lineheight - 4)) + pygame.draw.rect(surface, BRIGHT_GREEN, ( + pos[0] + hp.get_width() + 5, 4 + pos[1] + lineheight + 5 + 2, 50 * control_point.base.strength, lineheight - 4)) + + # Text + surface.blit(armor, (pos[0] + 4, 4 + pos[1] + lineheight * 2 + 10)) + surface.blit(aircraft, (pos[0] + 4, 4 + pos[1] + lineheight * 3 + 15)) + surface.blit(aa, (pos[0] + 4, 4 + pos[1] + lineheight * 4 + 20)) + + def draw_selected_event_info(self): + event = self.selected_event_info[0] + title = self.font.render(str(event), ANTIALIASING, BLACK, GREEN) + hint = self.font.render("Select CP to depart from.", ANTIALIASING, (225, 225, 225), BLACK) + + w = hint.get_width() + h = title.get_height() + hint.get_height() + 20 + + pos = self.overlay.get_width() / 2 - w / 2, self.overlay.get_height() - h + + # Draw frame + pygame.draw.rect(self.overlay, GREEN, (pos[0], pos[1], w + 8, h + 8)) + pygame.draw.rect(self.overlay, BLACK, (pos[0] + 2, pos[1] + 2, w + 4, h + 4)) + pygame.draw.rect(self.overlay, GREEN, (pos[0] + 2, pos[1], w + 4, title.get_height() + 4)) + + # Title + self.overlay.blit(title, (pos[0] + 4, 4 + pos[1])) + self.overlay.blit(hint, (pos[0] + 4, 4 + pos[1] + title.get_height() + 5)) + + def draw_ground_object(self, ground_object: TheaterGroundObject, surface: pygame.Surface, captured: bool, mouse_pos): + if captured: + color = self._player_color() + else: + color = self._enemy_color() + + x, y = self._transform_point(ground_object.position) + rect = pygame.Rect(x, y, 16, 16) + + if ground_object.is_dead or captured: + surface.blit(self.ground_assets_icons["cleared"], (x, y)) + else: + if ground_object.category in self.ground_assets_icons.keys(): + icon = self.ground_assets_icons[ground_object.category] + else: + icon = self.ground_assets_icons["target"] + surface.blit(icon, (x, y)) + + if rect.collidepoint(*mouse_pos): + self.draw_ground_object_info(ground_object, (x, y), color, surface) + + def draw_ground_object_info(self, ground_object: TheaterGroundObject, pos, color, surface: pygame.Surface): + lb = self.font.render(str(ground_object), ANTIALIASING, color, BLACK) + surface.blit(lb, (pos[0] + 18, pos[1])) + + def draw_events(self, surface: pygame.Surface, mouse_pos, mouse_down): + occupied_rects = [] + for cp in self.game.theater.controlpoints: + point = self._transform_point(cp.position) + occupied_rects.append(pygame.Rect(point[0] - 16, point[1] - 16, 32, 48)) + + def _location_to_rect(location: Point) -> pygame.Rect: + nonlocal occupied_rects + point = self._transform_point(location) + rect = pygame.Rect(point[0] - 16, point[1] - 16, 32, 32) + + i = 0 + while True: + result = True + for occupied_rect in occupied_rects: + if rect.colliderect(occupied_rect): + i += 1 + + if i % 2: + rect.y += occupied_rect.height + else: + rect.x += occupied_rect.width + + result = False + break + if result: + break + + occupied_rects.append(rect) + return rect + + def _events_priority_key(event: Event) -> int: + priority_list = [InfantryTransportEvent, StrikeEvent, BaseAttackEvent, UnitsDeliveryEvent] + if type(event) not in priority_list: + return 0 + else: + return priority_list.index(type(event)) + 1 + + events = self.game.events + events.sort(key=_events_priority_key, reverse=True) + + label_to_draw = None + for event in self.game.events: + location = event.location + if type(event) in [FrontlineAttackEvent, FrontlinePatrolEvent, ConvoyStrikeEvent]: + location = self._frontline_center(event.from_cp, event.to_cp) + + rect = _location_to_rect(location) + pygame.draw.rect(surface, EVENT_COLOR_ATTACK if event.is_player_attacking else EVENT_COLOR_DEFENSE, rect) + self.surface.blit(self.event_icons[event.__class__], rect.topleft) + + if rect.collidepoint(*mouse_pos) or self.selected_event_info == (event, rect.center): + if not label_to_draw: + label_to_draw = self.font.render(str(event), ANTIALIASING, WHITE, BLACK), rect.center + + if rect.collidepoint(*mouse_pos): + if mouse_down[0]: + self.selected_event_info = event, rect.center + mouse_down[0] = False + + if label_to_draw: + surface.blit(*label_to_draw) + + if self.selected_event_info: + self.draw_selected_event_info() + + return mouse_down + + def _selected_cp(self, cp): + if self.selected_event_info: + if self. _cp_available_for_selected_event(cp): + event = self.selected_event_info[0] + event.departure_cp = cp + + self.selected_event_info = None + self.parent.start_event(event) + else: + return + else: + self.parent.go_cp(cp) + + def _transform_point(self, p: Point, treshold=30) -> (int, int): point_a = list(self.game.theater.reference_points.keys())[0] point_a_img = self.game.theater.reference_points[point_a] @@ -44,101 +553,48 @@ class OverviewCanvas: X = point_b_img[1] + X_offset * X_scale Y = point_a_img[0] - Y_offset * Y_scale + X += MAP_PADDING + Y += MAP_PADDING + return X > treshold and X or treshold, Y > treshold and Y or treshold - def create_cp_title(self, coords, cp: ControlPoint): - title = cp.name - font = ("Helvetica", 10) + def _frontline_vector(self, from_cp: ControlPoint, to_cp: ControlPoint): + # Cache mechanism to avoid performing frontline vector computation on every frame + key = str(from_cp.id) + "_" + str(to_cp.id) + if key in self.frontline_vector_cache: + return self.frontline_vector_cache[key] + else: + frontline = Conflict.frontline_vector(from_cp, to_cp, self.game.theater) + self.frontline_vector_cache[key] = frontline + return frontline - id = self.canvas.create_text(coords[0], coords[1], text=title, font=font) - self.canvas.tag_bind(id, "", self.display(cp)) + def _frontline_center(self, from_cp: ControlPoint, to_cp: ControlPoint) -> typing.Optional[Point]: + frontline_vector = self._frontline_vector(from_cp, to_cp) + if frontline_vector: + return frontline_vector[0].point_from_heading(frontline_vector[1], frontline_vector[2]/2) + else: + return None - id = self.canvas.create_text(coords[0]+1, coords[1]+1, text=title, fill='white', font=font) - self.canvas.tag_bind(id, "", self.display(cp)) + def _cp_available_for_selected_event(self, cp: ControlPoint) -> bool: + event = self.selected_event_info[0] + return event.is_departure_available_from(cp) def _player_color(self): - return self.game.player == "USA" and "blue" or "red" + return self.game.player == "USA" and BLUE or RED def _enemy_color(self): - return self.game.player == "USA" and "red" or "blue" + return self.game.player == "USA" and RED or BLUE def update(self): - self.canvas.delete(ALL) - self.canvas.create_image((self.image.width()/2, self.image.height()/2), image=self.image) + self.redraw_required = True + self.draw() + self.budget_label.text = "Budget: {}m (+{}m)".format(self.game.budget, self.game.budget_reward_amount) - for cp in self.game.theater.controlpoints: - for ground_object in cp.ground_objects: - x, y = self.transform_point(ground_object.position) - self.canvas.create_text(x, - y, - text=".", - fill="black" if ground_object.is_dead else self._enemy_color(), - font=("Helvetica", 18)) - - coords = self.transform_point(cp.position) - for connected_cp in cp.connected_points: - connected_coords = self.transform_point(connected_cp.position) - if connected_cp.captured != cp.captured: - color = self._enemy_color() - elif connected_cp.captured and cp.captured: - color = self._player_color() - else: - color = "black" - - self.canvas.create_line((coords[0], coords[1], connected_coords[0], connected_coords[1]), width=2, fill=color) - - if cp.captured and not connected_cp.captured and Conflict.has_frontline_between(cp, connected_cp): - frontline = Conflict.frontline_vector(cp, connected_cp, self.game.theater) - if not frontline: - continue - - frontline_pos, heading, distance = frontline - if distance < 10000: - frontline_pos = frontline_pos.point_from_heading(heading + 180, 5000) - distance = 10000 - - start_coords = self.transform_point(frontline_pos, treshold=10) - end_coords = self.transform_point(frontline_pos.point_from_heading(heading, distance), treshold=60) - - self.canvas.create_line((*start_coords, *end_coords), width=2, fill=color) - - for cp in self.game.theater.controlpoints: - coords = self.transform_point(cp.position) - arc_size = 16 * math.pow(cp.importance, 1) - extent = max(cp.base.strength * 180, 10) - start = (180 - extent) / 2 - - if cp.captured: - color = self._player_color() - else: - color = self._enemy_color() - - cp_id = self.canvas.create_arc((coords[0] - arc_size/2, coords[1] - arc_size/2), - (coords[0] + arc_size/2, coords[1] + arc_size/2), - fill=color, - style=PIESLICE, - start=start, - extent=extent) - - """ - #For debugging purposes - - for r in cp.radials: - p = self.transform_point(cp.position.point_from_heading(r, 20000)) - self.canvas.create_text(p[0], p[1], text="{}".format(r)) - continue - """ - - self.canvas.tag_bind(cp_id, "", self.display(cp)) - self.create_cp_title((coords[0] + arc_size/4, coords[1] + arc_size/4), cp) - - units_title = "{}/{}/{}".format(cp.base.total_planes, cp.base.total_armor, cp.base.total_aa) - self.canvas.create_text(coords[0]+1, coords[1] - arc_size / 1.5 +1, text=units_title, font=("Helvetica", 8), fill=color) - self.canvas.create_text(coords[0], coords[1] - arc_size / 1.5, text=units_title, font=("Helvetica", 8), fill="white") + def compute_display_rules(self): + return sum([1 if a.get() else 0 for a in [self.display_forces, self.display_road, self.display_bases, self.display_ground_targets]]) def display(self, cp: ControlPoint): def action(_): return self.parent.go_cp(cp) return action - diff --git a/ui/window.py b/ui/window.py index f7766013..b5e8f7b4 100644 --- a/ui/window.py +++ b/ui/window.py @@ -1,8 +1,18 @@ from tkinter import * -from game.game import * +from tkinter import Menu as TkMenu +from tkinter import messagebox + from .styles import BG_COLOR,BG_TITLE_COLOR +from game.game import * +from theater import persiangulf, nevada, caucasus, start_generator +from userdata import logging as logging_module + +import sys +import webbrowser + class Window: + image = None left_pane = None # type: Frame right_pane = None # type: Frame @@ -10,11 +20,38 @@ class Window: def __init__(self): self.tk = Tk() self.tk.title("DCS Liberation") - self.tk.iconbitmap("icon.ico") + self.tk.iconbitmap("resources/icon.ico") self.tk.resizable(False, False) self.tk.grid_columnconfigure(0, weight=1) self.tk.grid_rowconfigure(0, weight=1) + self.frame = None + self.right_pane = None + self.left_pane = None + self.build() + + menubar = TkMenu(self.tk) + filemenu = TkMenu(menubar, tearoff=0) + filemenu.add_command(label="New Game", command=lambda: self.new_game_confirm()) + filemenu.add_separator() + filemenu.add_command(label="Exit", command=lambda: self.exit()) + menubar.add_cascade(label="File", menu=filemenu) + + helpmenu = TkMenu(menubar, tearoff=0) + helpmenu.add_command(label="Online Manual", command=lambda: webbrowser.open_new_tab("https://github.com/shdwp/dcs_liberation/wiki/Manual")) + helpmenu.add_command(label="Troubleshooting Guide", command=lambda: webbrowser.open_new_tab("https://github.com/shdwp/dcs_liberation/wiki/Troubleshooting")) + helpmenu.add_command(label="Modding Guide", command=lambda: webbrowser.open_new_tab("https://github.com/shdwp/dcs_liberation/wiki/Modding-tutorial")) + helpmenu.add_separator() + helpmenu.add_command(label="Contribute", command=lambda: webbrowser.open_new_tab("https://github.com/shdwp/dcs_liberation")) + helpmenu.add_command(label="Forum Thread", command=lambda: webbrowser.open_new_tab("https://forums.eagle.ru/showthread.php?t=214834")) + helpmenu.add_command(label="Report an issue", command=self.report_issue) + menubar.add_cascade(label="Help", menu=helpmenu) + + self.tk.config(menu=menubar) + self.tk.focus() + + + def build(self): self.frame = Frame(self.tk, bg=BG_COLOR) self.frame.grid(column=0, row=0, sticky=NSEW) self.frame.grid_columnconfigure(0) @@ -29,8 +66,6 @@ class Window: self.right_pane = Frame(self.frame, bg=BG_COLOR) self.right_pane.grid(row=0, column=1, sticky=NSEW) - self.tk.focus() - def clear_right_pane(self): for i in range(100): self.right_pane.grid_columnconfigure(1, weight=0) @@ -40,10 +75,70 @@ class Window: x.grid_remove() def clear(self): - for x in self.left_pane.winfo_children(): - x.grid_remove() - for x in self.right_pane.winfo_children(): - x.grid_remove() + def clear_recursive(x, n=50): + if n < 0: + return + for y in x.winfo_children(): + clear_recursive(y, n-1) + x.grid_forget() + + clear_recursive(self.frame, 50) + self.left_pane.grid_remove() + self.right_pane.grid_remove() + self.build() + + def start_new_game(self, player_name: str, enemy_name: str, terrain: str, sams: bool, midgame: bool, multiplier: float): + if terrain == "persiangulf": + conflicttheater = persiangulf.PersianGulfTheater() + elif terrain == "nevada": + conflicttheater = nevada.NevadaTheater() + else: + conflicttheater = caucasus.CaucasusTheater() + + if midgame: + for i in range(0, int(len(conflicttheater.controlpoints) / 2)): + conflicttheater.controlpoints[i].captured = True + + start_generator.generate_inital_units(conflicttheater, enemy_name, sams, multiplier) + start_generator.generate_groundobjects(conflicttheater) + game = Game(player_name=player_name, + enemy_name=enemy_name, + theater=conflicttheater) + game.budget = int(game.budget * multiplier) + game.settings.multiplier = multiplier + game.settings.sams = sams + game.settings.version = logging_module.version_string() + + if midgame: + game.budget = game.budget * 4 * len(list(conflicttheater.conflicts())) + + self.proceed_to_main_menu(game) + + def proceed_to_main_menu(self, game: Game): + from ui.mainmenu import MainMenu + self.clear() + m = MainMenu(self, None, game) + m.display() + + def proceed_to_new_game_menu(self): + from ui.newgamemenu import NewGameMenu + self.clear() + new_game_menu = NewGameMenu(self, self.start_new_game) + new_game_menu.display() + + def new_game_confirm(self): + result = messagebox.askquestion("Start a new game", "Are you sure you want to start a new game ? Your current campaign will be overriden and there is no going back !", icon='warning') + if result == 'yes': + self.proceed_to_new_game_menu() + else: + pass + + def report_issue(self): + raise logging_module.ShowLogsException() + + def exit(self): + self.tk.destroy() + sys.exit(0) def run(self): self.tk.mainloop() diff --git a/userdata/debriefing.py b/userdata/debriefing.py index f4f6251f..fe95f05a 100644 --- a/userdata/debriefing.py +++ b/userdata/debriefing.py @@ -42,6 +42,10 @@ def parse_mutliplayer_debriefing(contents: str): key = "initiator" if element is None: element = {} + elif line.startswith("initiatorMissionID\t"): + key = "initiatorMissionID" + if element is None: + element = {} elif line.startswith("type\t"): key = "type" if element is None: @@ -76,7 +80,7 @@ class Debriefing: nonlocal dead_units object_mission_id = int(object_mission_id_str) if object_mission_id in dead_units: - logging.info("debriefing: failed to append_dead_object {}: already exists!".format(object_mission_id)) + logging.error("debriefing: failed to append_dead_object {}: already exists!".format(object_mission_id)) return dead_units.append(object_mission_id) diff --git a/userdata/logging.py b/userdata/logging.py index 8fd944aa..a0c75d7f 100644 --- a/userdata/logging.py +++ b/userdata/logging.py @@ -35,6 +35,10 @@ def setup_version_string(str): _version_string = str +def version_string(): + return _version_string + + if "--stdout" in sys.argv: logging.basicConfig(stream=sys.stdout, level=logging.INFO) else: