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 1/3] 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) + From 1a89c3b2fb209b7bdf38489ee94f8a366212f464 Mon Sep 17 00:00:00 2001 From: EnvyDragon <138727357+EnvyDragon@users.noreply.github.com> Date: Mon, 13 May 2024 12:28:27 -0400 Subject: [PATCH 2/3] format/semantic changes --- worlds/sc2/PoolFilter.py | 2 +- worlds/sc2/Rules.py | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/worlds/sc2/PoolFilter.py b/worlds/sc2/PoolFilter.py index 514c0ef91c27..4aa1329cc667 100644 --- a/worlds/sc2/PoolFilter.py +++ b/worlds/sc2/PoolFilter.py @@ -235,7 +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 + enable_morphling = self.world.options.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 diff --git a/worlds/sc2/Rules.py b/worlds/sc2/Rules.py index 6fae9a4eeb47..c0373a500bdc 100644 --- a/worlds/sc2/Rules.py +++ b/worlds/sc2/Rules.py @@ -342,18 +342,15 @@ 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) - or self.morphling_enabled) \ + 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) - or self.morphling_enabled) \ + 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) - or self.morphling_enabled) \ + 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: From dd9c32f91ee61f44bb097dc79137301b196b1e68 Mon Sep 17 00:00:00 2001 From: EnvyDragon <138727357+EnvyDragon@users.noreply.github.com> Date: Mon, 13 May 2024 13:43:15 -0400 Subject: [PATCH 3/3] making option into proper bool in Rules.py --- worlds/sc2/Rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/sc2/Rules.py b/worlds/sc2/Rules.py index c0373a500bdc..13fef93e4a09 100644 --- a/worlds/sc2/Rules.py +++ b/worlds/sc2/Rules.py @@ -3,7 +3,7 @@ from BaseClasses import CollectionState from .Options import get_option_value, RequiredTactics, kerrigan_unit_available, AllInMap, \ GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence, \ - get_enabled_campaigns, MissionOrder + get_enabled_campaigns, MissionOrder, EnableMorphling from .Items import get_basic_units, defense_ratings, zerg_defense_ratings, kerrigan_actives, air_defense_ratings, \ kerrigan_levels, get_full_item_list from .MissionTables import SC2Race, SC2Campaign @@ -930,7 +930,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.morphling_enabled = get_option_value(world, "enable_morphling") == EnableMorphling.option_true 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)