From 2cfa9e902ea6f32acbc9f4e4d518a53ae18870e4 Mon Sep 17 00:00:00 2001 From: EnvyDragon <138727357+EnvyDragon@users.noreply.github.com> Date: Mon, 13 May 2024 11:34:04 -0400 Subject: [PATCH] sc2: Adding morphling option and logic --- worlds/sc2/Client.py | 10 ++++-- worlds/sc2/Options.py | 8 +++++ worlds/sc2/PoolFilter.py | 15 ++++---- worlds/sc2/Rules.py | 11 ++++-- worlds/sc2/test/test_generation.py | 55 ++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 12 deletions(-) diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index 846af6582924..da9c889fc1df 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -26,7 +26,7 @@ from worlds.sc2.ItemGroups import item_name_groups, unlisted_item_name_groups from worlds.sc2 import Options from worlds.sc2.Options import ( - MissionOrder, KerriganPrimalStatus, kerrigan_unit_available, KerriganPresence, + MissionOrder, KerriganPrimalStatus, kerrigan_unit_available, KerriganPresence, EnableMorphling, GameSpeed, GenericUpgradeItems, GenericUpgradeResearch, ColorChoice, GenericUpgradeMissions, LocationInclusion, ExtraLocations, MasteryLocations, ChallengeLocations, VanillaLocations, DisableForcedCamera, SkipCutscenes, GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, RequiredTactics, @@ -325,6 +325,7 @@ def _cmd_option(self, option_name: str = "", option_value: str = "") -> None: ConfigurableOptionInfo('supply_per_item', 'starting_supply_per_item', Options.StartingSupplyPerItem, ConfigurableOptionType.INTEGER), ConfigurableOptionInfo('no_forced_camera', 'disable_forced_camera', Options.DisableForcedCamera), ConfigurableOptionInfo('skip_cutscenes', 'skip_cutscenes', Options.SkipCutscenes), + ConfigurableOptionInfo('enable_morphling', 'enable_morphling', Options.EnableMorphling, can_break_logic=True), ) WARNING_COLOUR = "salmon" @@ -535,6 +536,7 @@ def __init__(self, *args, **kwargs) -> None: self.pending_color_update = False self.kerrigan_presence: int = KerriganPresence.default self.kerrigan_primal_status = 0 + self.enable_morphling = EnableMorphling.default self.mission_req_table: typing.Dict[SC2Campaign, typing.Dict[str, MissionInfo]] = {} self.final_mission: int = 29 self.announcements: queue.Queue = queue.Queue() @@ -629,6 +631,7 @@ def on_package(self, cmd: str, args: dict) -> None: self.kerrigan_levels_per_mission_completed = args["slot_data"].get("kerrigan_levels_per_mission_completed", 0) self.kerrigan_levels_per_mission_completed_cap = args["slot_data"].get("kerrigan_levels_per_mission_completed_cap", -1) self.kerrigan_total_level_cap = args["slot_data"].get("kerrigan_total_level_cap", -1) + self.enable_morphling = args["slot_data"].get("enable_morphling", EnableMorphling.option_false) self.grant_story_tech = args["slot_data"].get("grant_story_tech", GrantStoryTech.option_false) self.grant_story_levels = args["slot_data"].get("grant_story_levels", GrantStoryLevels.option_additive) self.required_tactics = args["slot_data"].get("required_tactics", RequiredTactics.option_standard) @@ -1046,7 +1049,7 @@ async def on_step(self, iteration: int): game_speed = self.ctx.game_speed_override else: game_speed = self.ctx.game_speed - await self.chat_send("?SetOptions {} {} {} {} {} {} {} {} {} {} {} {} {}".format( + await self.chat_send("?SetOptions {} {} {} {} {} {} {} {} {} {} {} {} {} {}".format( difficulty, self.ctx.generic_upgrade_research, self.ctx.all_in_choice, @@ -1059,7 +1062,8 @@ async def on_step(self, iteration: int): soa_options, self.ctx.mission_order, 1 if self.ctx.nova_covert_ops_only else 0, - self.ctx.grant_story_levels + self.ctx.grant_story_levels, + self.ctx.enable_morphling )) await self.chat_send("?GiveResources {} {} {}".format( start_items[SC2Race.ANY][0], diff --git a/worlds/sc2/Options.py b/worlds/sc2/Options.py index c340cefe034e..31c7d2e03891 100644 --- a/worlds/sc2/Options.py +++ b/worlds/sc2/Options.py @@ -494,6 +494,13 @@ class KerriganPrimalStatus(Choice): option_half_completion = 4 option_item = 5 +class EnableMorphling(Toggle): + """ + Determines whether the player can build Morphlings, which allow for inefficient morphing of advanced units + like Ravagers and Lurkers without requiring the base unit to be unlocked first. + """ + display_name = "Enable Morphling" + class SpearOfAdunPresence(Choice): """ @@ -860,6 +867,7 @@ class Starcraft2Options(PerGameCommonOptions): kerrigan_total_level_cap: KerriganTotalLevelCap start_primary_abilities: StartPrimaryAbilities kerrigan_primal_status: KerriganPrimalStatus + enable_morphling: EnableMorphling spear_of_adun_presence: SpearOfAdunPresence spear_of_adun_present_in_no_build: SpearOfAdunPresentInNoBuild spear_of_adun_autonomously_cast_ability_presence: SpearOfAdunAutonomouslyCastAbilityPresence diff --git a/worlds/sc2/PoolFilter.py b/worlds/sc2/PoolFilter.py index 1a88599966fa..514c0ef91c27 100644 --- a/worlds/sc2/PoolFilter.py +++ b/worlds/sc2/PoolFilter.py @@ -10,7 +10,7 @@ from .Options import (get_option_value, MissionOrder, get_enabled_campaigns, RequiredTactics, kerrigan_unit_available, GrantStoryTech, TakeOverAIAllies, campaign_depending_orders, - ShuffleCampaigns, get_excluded_missions, ShuffleNoBuild, ExtraLocations, GrantStoryLevels, + ShuffleCampaigns, get_excluded_missions, ShuffleNoBuild, ExtraLocations, GrantStoryLevels, EnableMorphling ) from . import ItemNames, ItemGroups @@ -235,6 +235,7 @@ def generate_reduced_inventory(self, inventory_size: int, mission_requirements: """Attempts to generate a reduced inventory that can fulfill the mission requirements.""" inventory: List[Item] = list(self.item_pool) locked_items: List[Item] = list(self.locked_items) + enable_morphling = get_option_value(self.world, "enable_morphling") == EnableMorphling.option_true item_list = get_full_item_list() self.logical_inventory = [ item.name for item in inventory + locked_items + self.existing_items @@ -422,15 +423,17 @@ def attempt_removal(item: Item) -> bool: if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory and ItemNames.ZERGLING not in self.logical_inventory and ItemNames.KERRIGAN_SPAWN_BANELINGS not in self.logical_inventory + and not enable_morphling ): inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT] unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT] unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT] - # Spawn Banelings without Zergling => remove Baneling unit, keep upgrades except macro ones + # Spawn Banelings without Zergling/Morphling => remove Baneling unit, keep upgrades except macro ones if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory and ItemNames.ZERGLING not in self.logical_inventory and ItemNames.KERRIGAN_SPAWN_BANELINGS in self.logical_inventory + and not enable_morphling ): inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT] inventory = [item for item in inventory if item.name != ItemNames.BANELING_RAPID_METAMORPH] @@ -440,8 +443,8 @@ def attempt_removal(item: Item) -> bool: inventory = [item for item in inventory if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)] locked_items = [item for item in locked_items if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)] unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)] - # T3 items removal rules - remove morph and its upgrades if the basic unit isn't in - if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR} & logical_inventory_set: + # T3 items removal rules - remove morph and its upgrades if the basic unit isn't in and morphling is unavailable + if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR} & logical_inventory_set and not enable_morphling: inventory = [item for item in inventory if not item.name.endswith("(Mutalisk/Corruptor)")] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT] @@ -452,12 +455,12 @@ def attempt_removal(item: Item) -> bool: unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT] unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT] unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT] - if ItemNames.ROACH not in logical_inventory_set: + if ItemNames.ROACH not in logical_inventory_set and not enable_morphling: inventory = [item for item in inventory if item.name != ItemNames.ROACH_RAVAGER_ASPECT] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT] unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ROACH_RAVAGER_ASPECT] unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT] - if ItemNames.HYDRALISK not in logical_inventory_set: + if ItemNames.HYDRALISK not in logical_inventory_set and not enable_morphling: inventory = [item for item in inventory if not item.name.endswith("(Hydralisk)")] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_IMPALER_ASPECT] diff --git a/worlds/sc2/Rules.py b/worlds/sc2/Rules.py index c51b1f5a25db..6fae9a4eeb47 100644 --- a/worlds/sc2/Rules.py +++ b/worlds/sc2/Rules.py @@ -342,15 +342,19 @@ def zerg_basic_anti_air(self, state: CollectionState) -> bool: state.has_any({ItemNames.SWARM_QUEEN, ItemNames.SCOURGE}, self.player) or (self.advanced_tactics and state.has(ItemNames.SPORE_CRAWLER, self.player)) def morph_brood_lord(self, state: CollectionState) -> bool: - return state.has_any({ItemNames.MUTALISK, ItemNames.CORRUPTOR}, self.player) \ + return (state.has_any({ItemNames.MUTALISK, ItemNames.CORRUPTOR}, self.player) + or self.morphling_enabled) \ and state.has(ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, self.player) def morph_viper(self, state: CollectionState) -> bool: - return state.has_any({ItemNames.MUTALISK, ItemNames.CORRUPTOR}, self.player) \ + return (state.has_any({ItemNames.MUTALISK, ItemNames.CORRUPTOR}, self.player) + or self.morphling_enabled) \ and state.has(ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT, self.player) def morph_impaler_or_lurker(self, state: CollectionState) -> bool: - return state.has(ItemNames.HYDRALISK, self.player) and state.has_any({ItemNames.HYDRALISK_IMPALER_ASPECT, ItemNames.HYDRALISK_LURKER_ASPECT}, self.player) + return (state.has(ItemNames.HYDRALISK, self.player) + or self.morphling_enabled) \ + and state.has_any({ItemNames.HYDRALISK_IMPALER_ASPECT, ItemNames.HYDRALISK_LURKER_ASPECT}, self.player) def zerg_competent_comp(self, state: CollectionState) -> bool: advanced = self.advanced_tactics @@ -929,6 +933,7 @@ def __init__(self, world: World): self.kerrigan_levels_per_mission_completed = get_option_value(world, "kerrigan_levels_per_mission_completed") self.kerrigan_levels_per_mission_completed_cap = get_option_value(world, "kerrigan_levels_per_mission_completed_cap") self.kerrigan_total_level_cap = get_option_value(world, "kerrigan_total_level_cap") + self.morphling_enabled = get_option_value(world, "enable_morphling") self.story_tech_granted = get_option_value(world, "grant_story_tech") == GrantStoryTech.option_true self.story_levels_granted = get_option_value(world, "grant_story_levels") != GrantStoryLevels.option_disabled self.basic_terran_units = get_basic_units(world, SC2Race.TERRAN) diff --git a/worlds/sc2/test/test_generation.py b/worlds/sc2/test/test_generation.py index 4960d0f80ed5..07a8b2bd9d33 100644 --- a/worlds/sc2/test/test_generation.py +++ b/worlds/sc2/test/test_generation.py @@ -359,3 +359,58 @@ def test_lotv_only_doesnt_include_kerrigan_items_with_grant_story_tech(self) -> self.assertFalse(kerrigan_items_in_pool) kerrigan_passives_in_pool = set(ItemGroups.kerrigan_passives).intersection(item_names) self.assertFalse(kerrigan_passives_in_pool) + + def test_excluding_zerg_units_with_morphling_enabled_doesnt_exclude_aspects(self) -> None: + options = { + 'enable_wol_missions': False, + 'enable_prophecy_missions': False, + 'enable_hots_missions': True, + 'enable_lotv_prologue_missions': False, + 'enable_lotv_missions': False, + 'enable_epilogue_missions': False, + 'enable_nco_missions': False, + 'required_tactics': Options.RequiredTactics.option_no_logic, + 'enable_morphling': Options.EnableMorphling.option_true, + 'excluded_items': [ + ItemGroups.ItemGroupNames.ZERG_UNITS.lower() + ], + 'unexcluded_items': [ + ItemGroups.ItemGroupNames.ZERG_MORPHS.lower() + ] + } + self.generate_world(options) + item_names = [item.name for item in self.multiworld.itempool] + self.assertTrue(item_names) + aspects_in_pool = list(set(item_names).intersection(set(ItemGroups.zerg_morphs))) + self.assertTrue(aspects_in_pool) + units_in_pool = list(set(item_names).intersection(set(ItemGroups.zerg_units)) + .difference(set(ItemGroups.zerg_morphs))) + self.assertFalse(units_in_pool) + + def test_excluding_zerg_units_with_morphling_disabled_should_exclude_aspects(self) -> None: + options = { + 'enable_wol_missions': False, + 'enable_prophecy_missions': False, + 'enable_hots_missions': True, + 'enable_lotv_prologue_missions': False, + 'enable_lotv_missions': False, + 'enable_epilogue_missions': False, + 'enable_nco_missions': False, + 'required_tactics': Options.RequiredTactics.option_no_logic, + 'enable_morphling': Options.EnableMorphling.option_false, + 'excluded_items': [ + ItemGroups.ItemGroupNames.ZERG_UNITS.lower() + ], + 'unexcluded_items': [ + ItemGroups.ItemGroupNames.ZERG_MORPHS.lower() + ] + } + self.generate_world(options) + item_names = [item.name for item in self.multiworld.itempool] + self.assertTrue(item_names) + aspects_in_pool = list(set(item_names).intersection(set(ItemGroups.zerg_morphs))) + self.assertFalse(aspects_in_pool) + units_in_pool = list(set(item_names).intersection(set(ItemGroups.zerg_units)) + .difference(set(ItemGroups.zerg_morphs))) + self.assertFalse(units_in_pool) +