diff --git a/worlds/sc2/items.py b/worlds/sc2/items.py index 4d23ace2246e..f1cdbc3e37f5 100644 --- a/worlds/sc2/items.py +++ b/worlds/sc2/items.py @@ -1962,9 +1962,9 @@ def get_basic_units(world: 'SC2World', race: SC2Race) -> typing.Set[str]: # Defense rating table # Commented defense ratings are handled in LogicMixin -defense_ratings = { +tvx_defense_ratings = { item_names.SIEGE_TANK: 5, - # "Maelstrom Rounds": 2, + # "Graduating Range": 1, item_names.PLANETARY_FORTRESS: 3, # Bunker w/ Marine/Marauder: 3, item_names.PERDITION_TURRET: 2, @@ -1976,16 +1976,64 @@ def get_basic_units(world: 'SC2World', race: SC2Race) -> typing.Set[str]: item_names.WIDOW_MINE: 1, # "Concealment (Widow Mine)": 1 } -zerg_defense_ratings = { +tvz_defense_ratings = { item_names.PERDITION_TURRET: 2, # Bunker w/ Firebat: 2, item_names.LIBERATOR: -2, item_names.HIVE_MIND_EMULATOR: 3, item_names.PSI_DISRUPTER: 3, } -air_defense_ratings = { +tvx_air_defense_ratings = { item_names.MISSILE_TURRET: 2, } +zvx_defense_ratings = { + # Note that this doesn't include Kerrigan because this is just for race swaps, which doesn't involve her (for now) + item_names.SPINE_CRAWLER: 2, + # w/ Twin Drones: 1 + item_names.SWARM_QUEEN: 1, + item_names.SWARM_HOST: 1, + # impaler: 3 + # "Hardened Tentacle Spines (Impaler)": 2 + # lurker: 1 + # "Seismic Spines (Lurker)": 2 + # "Adapted Spines (Lurker)": 1 + # brood lord : 2 + # corpser roach: 1 + # creep tumors (swarm queen or overseer): 1 + # w/ malignant creep: 1 + item_names.INFESTED_SIEGE_TANKS: 5, + # "Graduating Range": 1, +} +# zvz_defense_ratings = { + # corpser roach: 1 + # primal igniter: 2 + # w/ concentrated fire: 1 + # lurker: 1 + # w/ adapted spines: -1 + # impaler: -1 +# } +zvx_air_defense_ratings = { + item_names.SPORE_CRAWLER: 2, + # w/ Twin Drones: 1 +} +pvx_defense_ratings = { + item_names.PHOTON_CANNON: 2, + item_names.KHAYDARIN_MONOLITH: 3, + # khalai ingenuity, enhanced targeting, optimized ordnance: 1 each if either of the above exist + item_names.SHIELD_BATTERY: 1, + item_names.NEXUS_OVERCHARGE: 2, + item_names.CORSAIR: 1, + # any of cloak, argus jewel, sustaining disruption: 1 + item_names.MATRIX_OVERLOAD: 1, + item_names.COLOSSUS: 1, +} +pvz_defense_ratings = { + item_names.KHAYDARIN_MONOLITH: -2, + item_names.COLOSSUS: 2, + item_names.VANGUARD: 1, + item_names.REAVER: 1, + # any high templar variant: 2 +} kerrigan_levels = [ item_name for item_name, item_data in item_table.items() diff --git a/worlds/sc2/locations.py b/worlds/sc2/locations.py index 9a9f806a1b45..4f3f4237e30f 100644 --- a/worlds/sc2/locations.py +++ b/worlds/sc2/locations.py @@ -1698,8 +1698,86 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: lambda state: logic.protoss_common_unit(state)), make_location_data(SC2Mission.ZERO_HOUR_P.mission_name, "Cavalry's on the Way", SC2_RACESWAP_LOC_ID_OFFSET + 610, LocationType.EXTRA, lambda state: logic.protoss_common_unit(state)), - # 70X/80X - Evacuation - # 90X/100X - Outbreak + make_location_data(SC2Mission.EVACUATION_Z.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 700, LocationType.VICTORY, + lambda state: logic.zerg_common_unit(state) and + (adv_tactics and logic.zerg_basic_kerriganless_anti_air(state) + or logic.zerg_competent_anti_air(state))), + make_location_data(SC2Mission.EVACUATION_Z.mission_name, "North Chrysalis", SC2_RACESWAP_LOC_ID_OFFSET + 701, LocationType.VANILLA), + make_location_data(SC2Mission.EVACUATION_Z.mission_name, "West Chrysalis", SC2_RACESWAP_LOC_ID_OFFSET + 702, LocationType.VANILLA, + lambda state: logic.zerg_common_unit(state)), + make_location_data(SC2Mission.EVACUATION_Z.mission_name, "East Chrysalis", SC2_RACESWAP_LOC_ID_OFFSET + 703, LocationType.VANILLA, + lambda state: logic.zerg_common_unit(state)), + make_location_data(SC2Mission.EVACUATION_Z.mission_name, "Reach Hanson", SC2_RACESWAP_LOC_ID_OFFSET + 704, LocationType.EXTRA), + make_location_data(SC2Mission.EVACUATION_Z.mission_name, "Secret Resource Stash", SC2_RACESWAP_LOC_ID_OFFSET + 705, LocationType.EXTRA), + make_location_data(SC2Mission.EVACUATION_Z.mission_name, "Flawless", SC2_RACESWAP_LOC_ID_OFFSET + 706, LocationType.CHALLENGE, + lambda state: logic.zerg_common_unit(state) and + logic.zerg_competent_defense(state) and + (adv_tactics and logic.zerg_basic_kerriganless_anti_air(state) + or logic.zerg_competent_anti_air(state))), + make_location_data(SC2Mission.EVACUATION_P.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 800, LocationType.VICTORY, + lambda state: logic.protoss_common_unit(state) and + (adv_tactics and logic.protoss_basic_anti_air(state) + or logic.protoss_competent_anti_air(state))), + make_location_data(SC2Mission.EVACUATION_P.mission_name, "North Chrysalis", SC2_RACESWAP_LOC_ID_OFFSET + 801, LocationType.VANILLA), + make_location_data(SC2Mission.EVACUATION_P.mission_name, "West Chrysalis", SC2_RACESWAP_LOC_ID_OFFSET + 802, LocationType.VANILLA, + lambda state: logic.protoss_common_unit(state)), + make_location_data(SC2Mission.EVACUATION_P.mission_name, "East Chrysalis", SC2_RACESWAP_LOC_ID_OFFSET + 803, LocationType.VANILLA, + lambda state: logic.protoss_common_unit(state)), + make_location_data(SC2Mission.EVACUATION_P.mission_name, "Reach Hanson", SC2_RACESWAP_LOC_ID_OFFSET + 804, LocationType.EXTRA), + make_location_data(SC2Mission.EVACUATION_P.mission_name, "Secret Resource Stash", SC2_RACESWAP_LOC_ID_OFFSET + 805, LocationType.EXTRA), + make_location_data(SC2Mission.EVACUATION_P.mission_name, "Flawless", SC2_RACESWAP_LOC_ID_OFFSET + 806, LocationType.CHALLENGE, + lambda state: logic.protoss_defense_rating(state, True) >= 2 and + logic.protoss_common_unit(state) and + (adv_tactics and logic.protoss_basic_anti_air(state) + or logic.protoss_competent_anti_air(state))), + make_location_data(SC2Mission.OUTBREAK_Z.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 900, LocationType.VICTORY, + lambda state: logic.zerg_defense_rating(state, True, False) >= 4 and + logic.zerg_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_Z.mission_name, "Left Infestor", SC2_RACESWAP_LOC_ID_OFFSET + 901, LocationType.VANILLA, + lambda state: logic.zerg_defense_rating(state, True, False) >= 2 and + logic.zerg_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_Z.mission_name, "Right Infestor", SC2_RACESWAP_LOC_ID_OFFSET + 902, LocationType.VANILLA, + lambda state: logic.zerg_defense_rating(state, True, False) >= 2 and + logic.zerg_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_Z.mission_name, "North Infested Command Center", SC2_RACESWAP_LOC_ID_OFFSET + 903, LocationType.EXTRA, + lambda state: logic.zerg_defense_rating(state, True, False) >= 2 and + logic.zerg_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_Z.mission_name, "South Infested Command Center", SC2_RACESWAP_LOC_ID_OFFSET + 904, LocationType.EXTRA, + lambda state: logic.zerg_defense_rating(state, True, False) >= 2 and + logic.zerg_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_Z.mission_name, "Northwest Bar", SC2_RACESWAP_LOC_ID_OFFSET + 905, LocationType.EXTRA, + lambda state: logic.zerg_defense_rating(state, True, False) >= 2 and + logic.zerg_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_Z.mission_name, "North Bar", SC2_RACESWAP_LOC_ID_OFFSET + 906, LocationType.EXTRA, + lambda state: logic.zerg_defense_rating(state, True, False) >= 2 and + logic.zerg_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_Z.mission_name, "South Bar", SC2_RACESWAP_LOC_ID_OFFSET + 907, LocationType.EXTRA, + lambda state: logic.zerg_defense_rating(state, True, False) >= 2 and + logic.zerg_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_P.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 1000, LocationType.VICTORY, + lambda state: logic.protoss_defense_rating(state, True) >= 4 and + logic.protoss_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_P.mission_name, "Left Infestor", SC2_RACESWAP_LOC_ID_OFFSET + 1001, LocationType.VANILLA, + lambda state: logic.protoss_defense_rating(state, True) >= 2 and + logic.protoss_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_P.mission_name, "Right Infestor", SC2_RACESWAP_LOC_ID_OFFSET + 1002, LocationType.VANILLA, + lambda state: logic.protoss_defense_rating(state, True) >= 2 and + logic.protoss_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_P.mission_name, "North Infested Command Center", SC2_RACESWAP_LOC_ID_OFFSET + 1003, LocationType.EXTRA, + lambda state: logic.protoss_defense_rating(state, True) >= 2 and + logic.protoss_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_P.mission_name, "South Infested Command Center", SC2_RACESWAP_LOC_ID_OFFSET + 1004, LocationType.EXTRA, + lambda state: logic.protoss_defense_rating(state, True) >= 2 and + logic.protoss_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_P.mission_name, "Northwest Bar", SC2_RACESWAP_LOC_ID_OFFSET + 1005, LocationType.EXTRA, + lambda state: logic.protoss_defense_rating(state, True) >= 2 and + logic.protoss_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_P.mission_name, "North Bar", SC2_RACESWAP_LOC_ID_OFFSET + 1006, LocationType.EXTRA, + lambda state: logic.protoss_defense_rating(state, True) >= 2 and + logic.protoss_common_unit(state)), + make_location_data(SC2Mission.OUTBREAK_P.mission_name, "South Bar", SC2_RACESWAP_LOC_ID_OFFSET + 1007, LocationType.EXTRA, + lambda state: logic.protoss_defense_rating(state, True) >= 2 and + logic.protoss_common_unit(state)), # 110X/120X - Safe Haven # 130X/140X - Haven's Fall make_location_data(SC2Mission.SMASH_AND_GRAB_Z.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 1500, LocationType.VICTORY, diff --git a/worlds/sc2/mission_tables.py b/worlds/sc2/mission_tables.py index 265dee7f8c76..2a4d7798371b 100644 --- a/worlds/sc2/mission_tables.py +++ b/worlds/sc2/mission_tables.py @@ -103,8 +103,8 @@ def __init__(self, mission_id: int, name: str, campaign: SC2Campaign, area: str, LIBERATION_DAY = 1, "Liberation Day", SC2Campaign.WOL, "Mar Sara", SC2Race.ANY, MissionPools.STARTER, "ap_liberation_day", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsTerran THE_OUTLAWS = 2, "The Outlaws (Terran)", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_the_outlaws", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.HasRaceSwap ZERO_HOUR = 3, "Zero Hour (Terran)", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_zero_hour", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap - EVACUATION = 4, "Evacuation", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_evacuation", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsZerg - OUTBREAK = 5, "Outbreak", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_outbreak", MissionFlag.Terran|MissionFlag.Defense|MissionFlag.VsZerg + EVACUATION = 4, "Evacuation (Terran)", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_evacuation", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + OUTBREAK = 5, "Outbreak (Terran)", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_outbreak", MissionFlag.Terran|MissionFlag.Defense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap SAFE_HAVEN = 6, "Safe Haven", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_safe_haven", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsProtoss HAVENS_FALL = 7, "Haven's Fall", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_havens_fall", MissionFlag.Terran|MissionFlag.VsZerg SMASH_AND_GRAB = 8, "Smash and Grab (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.EASY, "ap_smash_and_grab", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsPZ|MissionFlag.HasRaceSwap @@ -202,8 +202,10 @@ def __init__(self, mission_id: int, name: str, campaign: SC2Campaign, area: str, THE_OUTLAWS_P = 87, "The Outlaws (Protoss)", SC2Campaign.WOL, "Mar Sara", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_outlaws", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap ZERO_HOUR_Z = 88, "Zero Hour (Zerg)", SC2Campaign.WOL, "Mar Sara", SC2Race.ZERG, MissionPools.EASY, "ap_zero_hour", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap ZERO_HOUR_P = 89, "Zero Hour (Protoss)", SC2Campaign.WOL, "Mar Sara", SC2Race.PROTOSS, MissionPools.EASY, "ap_zero_hour", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap - # 90/91 - Evacuation - # 92/93 - Outbreak + EVACUATION_Z = 90, "Evacuation (Zerg)", SC2Campaign.WOL, "Colonist", SC2Race.ZERG, MissionPools.EASY, "ap_evacuation", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.RaceSwap + EVACUATION_P = 91, "Evacuation (Protoss)", SC2Campaign.WOL, "Colonist", SC2Race.PROTOSS, MissionPools.EASY, "ap_evacuation", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.RaceSwap + OUTBREAK_Z = 92, "Outbreak (Zerg)", SC2Campaign.WOL, "Colonist", SC2Race.ZERG, MissionPools.EASY, "ap_outbreak", MissionFlag.Zerg|MissionFlag.Defense|MissionFlag.VsZerg|MissionFlag.RaceSwap + OUTBREAK_P = 93, "Outbreak (Protoss)", SC2Campaign.WOL, "Colonist", SC2Race.PROTOSS, MissionPools.EASY, "ap_outbreak", MissionFlag.Protoss|MissionFlag.Defense|MissionFlag.VsZerg|MissionFlag.RaceSwap # 94/95 - Safe Haven # 96/97 - Haven's Fall SMASH_AND_GRAB_Z = 98, "Smash and Grab (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.EASY, "ap_smash_and_grab", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsPZ|MissionFlag.RaceSwap diff --git a/worlds/sc2/options.py b/worlds/sc2/options.py index a3e227f9fd6f..81eb7e868792 100644 --- a/worlds/sc2/options.py +++ b/worlds/sc2/options.py @@ -149,7 +149,7 @@ class MaximumCampaignSize(Range): """ display_name = "Maximum Campaign Size" range_start = 1 - range_end = 89 + range_end = 93 default = 83 diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py index 48f0e49c51d0..131cfc55b62b 100644 --- a/worlds/sc2/rules.py +++ b/worlds/sc2/rules.py @@ -4,8 +4,9 @@ from .options import get_option_value, RequiredTactics, kerrigan_unit_available, AllInMap, \ GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence, \ get_enabled_campaigns, MissionOrder, EnableMorphling, get_enabled_races -from .items import get_basic_units, defense_ratings, zerg_defense_ratings, kerrigan_actives, air_defense_ratings, \ - kerrigan_levels, get_full_item_list +from .items import get_basic_units, tvx_defense_ratings, tvz_defense_ratings, kerrigan_actives, tvx_air_defense_ratings, \ + kerrigan_levels, get_full_item_list, zvx_air_defense_ratings, zvx_defense_ratings, pvx_defense_ratings, \ + pvz_defense_ratings from .mission_tables import SC2Race, SC2Campaign from . import item_names @@ -130,7 +131,7 @@ def terran_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_en :param air_enemy: :return: """ - defense_score = sum((defense_ratings[item] for item in defense_ratings if state.has(item, self.player))) + defense_score = sum((tvx_defense_ratings[item] for item in tvx_defense_ratings if state.has(item, self.player))) # Manned Bunker if state.has_any({item_names.MARINE, item_names.MARAUDER}, self.player) and state.has(item_names.BUNKER, self.player): defense_score += 3 @@ -150,9 +151,9 @@ def terran_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_en # General enemy-based rules if zerg_enemy: - defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if state.has(item, self.player))) + defense_score += sum((tvz_defense_ratings[item] for item in tvz_defense_ratings if state.has(item, self.player))) if air_enemy: - defense_score += sum((air_defense_ratings[item] for item in air_defense_ratings if state.has(item, self.player))) + defense_score += sum((tvx_air_defense_ratings[item] for item in tvx_air_defense_ratings if state.has(item, self.player))) if air_enemy and zerg_enemy and state.has(item_names.VALKYRIE, self.player): # Valkyries shred mass Mutas, most common air enemy that's massed in these cases defense_score += 2 @@ -338,6 +339,69 @@ def all_in_requirement(self, state: CollectionState): and state.has_any({item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER, item_names.MISSILE_TURRET}, self.player) # HotS + + def zerg_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_enemy: bool = True) -> int: + """ + Ability to handle defensive missions + :param state: + :param zerg_enemy: + :param air_enemy: + :return: + """ + defense_score = sum((zvx_defense_ratings[item] for item in zvx_defense_ratings if state.has(item, self.player))) + # Twin Drones + if state.has(item_names.TWIN_DRONES, self.player): + if state.has(item_names.SPINE_CRAWLER, self.player): + defense_score += 1 + if state.has(item_names.SPORE_CRAWLER, self.player) and air_enemy: + defense_score += 1 + # Impaler + if self.morph_impaler(state): + defense_score += 3 + if state.has(item_names.IMPALER_HARDENED_TENTACLE_SPINES, self.player): + defense_score += 1 + if zerg_enemy: + defense_score += -1 + # Lurker + if self.morph_lurker(state): + defense_score += 1 + if state.has(item_names.LURKER_SEISMIC_SPINES, self.player): + defense_score += 2 + if state.has(item_names.LURKER_ADAPTED_SPINES, self.player) and not zerg_enemy: + defense_score += 1 + if zerg_enemy: + defense_score += 1 + # Brood Lord + if self.morph_brood_lord(state): + defense_score += 2 + # Corpser Roach + if state.has_all({item_names.ROACH, item_names.ROACH_CORPSER_STRAIN}, self.player): + defense_score += 1 + if zerg_enemy: + defense_score += 1 + # Igniter + if self.morph_igniter(state) and zerg_enemy: + defense_score += 2 + if state.has(item_names.PRIMAL_IGNITER_CONCENTRATED_FIRE, self.player): + defense_score += 1 + # Creep Tumors + if state.has_any({item_names.SWARM_QUEEN, item_names.OVERLORD_OVERSEER_ASPECT}, self.player): + if not zerg_enemy: + defense_score += 1 + if state.has(item_names.MALIGNANT_CREEP, self.player): + defense_score += 1 + # Infested Siege Tanks + if state.has_all({item_names.INFESTED_SIEGE_TANKS, item_names.SIEGE_TANK_GRADUATING_RANGE}, self.player): + defense_score += 1 + + # General enemy-based rules + if air_enemy: + defense_score += sum((zvx_air_defense_ratings[item] for item in zvx_air_defense_ratings if state.has(item, self.player))) + # Advanced Tactics bumps defense rating requirements down by 2 + if self.advanced_tactics: + defense_score += 2 + return defense_score + def zerg_common_unit(self, state: CollectionState) -> bool: return state.has_any(self.basic_zerg_units, self.player) @@ -362,9 +426,20 @@ def morph_viper(self, state: CollectionState) -> bool: return (state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player) or self.morphling_enabled) \ and state.has(item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT, self.player) - def morph_impaler_or_lurker(self, state: CollectionState) -> bool: + def morph_impaler(self, state: CollectionState) -> bool: return (state.has(item_names.HYDRALISK, self.player) or self.morphling_enabled) \ - and state.has_any({item_names.HYDRALISK_IMPALER_ASPECT, item_names.HYDRALISK_LURKER_ASPECT}, self.player) + and state.has(item_names.HYDRALISK_IMPALER_ASPECT, self.player) + + def morph_lurker(self, state: CollectionState) -> bool: + return (state.has(item_names.HYDRALISK, self.player) or self.morphling_enabled) \ + and state.has(item_names.HYDRALISK_LURKER_ASPECT, self.player) + + def morph_impaler_or_lurker(self, state: CollectionState) -> bool: + return self.morph_impaler(state) or self.morph_lurker(state) + + def morph_igniter(self, state: CollectionState) -> bool: + return (state.has(item_names.ROACH, self.player) or self.morphling_enabled) \ + and state.has(item_names.ROACH_PRIMAL_IGNITER_ASPECT, self.player) def zerg_competent_comp(self, state: CollectionState) -> bool: advanced = self.advanced_tactics @@ -472,6 +547,35 @@ def the_reckoning_requirement(self, state: CollectionState) -> bool: # LotV + def protoss_defense_rating(self, state: CollectionState, zerg_enemy: bool) -> int: + """ + Ability to handle defensive missions + :param state: + :param zerg_enemy: + :param air_enemy: + :return: + """ + defense_score = sum((pvx_defense_ratings[item] for item in pvx_defense_ratings if state.has(item, self.player))) + # Defensive Upgrades + defensive_upgrades = {item_names.KHALAI_INGENUITY, item_names.ENHANCED_TARGETING, item_names.OPTIMIZED_ORDNANCE} + if state.has_any({item_names.PHOTON_CANNON, item_names.KHAYDARIN_MONOLITH}, self.player): + defense_score += sum(item for item in defensive_upgrades if state.has(item, self.player)) + # Corsair upgrades + if state.has(item_names.CORSAIR, self.player) and state.has_any({item_names.CORSAIR_ARGUS_JEWEL, item_names.CORSAIR_STEALTH_DRIVE, item_names.CORSAIR_SUSTAINING_DISRUPTION}, self.player): + defense_score += 1 + # High Templar variants vs zerg + if state.has_any({item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.ASCENDANT}, self.player) and zerg_enemy: + defense_score += 2 + + # General enemy-based rules + # No anti-air defense dict here, use an existing logic rule instead + if zerg_enemy: + defense_score += sum((pvz_defense_ratings[item] for item in pvz_defense_ratings if state.has(item, self.player))) + # Advanced Tactics bumps defense rating requirements down by 2 + if self.advanced_tactics: + defense_score += 2 + return defense_score + def protoss_common_unit(self, state: CollectionState) -> bool: return state.has_any(self.basic_protoss_units, self.player)