diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index 5b9134821b11..1470fab620fb 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -19,7 +19,7 @@ from .options import (get_option_value, LocationInclusion, KerriganLevelItemDistribution, KerriganPresence, KerriganPrimalStatus, kerrigan_unit_available, StarterUnit, SpearOfAdunPresence, get_enabled_campaigns, SpearOfAdunAutonomouslyCastAbilityPresence, Starcraft2Options, - GrantStoryTech, GenericUpgradeResearch, + GrantStoryTech, GenericUpgradeResearch, get_enabled_races ) from .pool_filter import filter_items from .mission_tables import ( @@ -246,6 +246,7 @@ def resolve_count(count: Optional[int], max_count: int) -> int: def flag_excludes_by_faction_presence(world: SC2World, item_list: List[FilterItem]) -> None: """Excludes items based on if their faction has a mission present where they can be used""" missions = get_all_missions(world.mission_req_table) + races = get_enabled_races(world) if world.options.take_over_ai_allies.value: terran_missions = [mission for mission in missions if (MissionFlag.Terran|MissionFlag.AiTerranAlly) & mission.flags] zerg_missions = [mission for mission in missions if (MissionFlag.Zerg|MissionFlag.AiZergAlly) & mission.flags] @@ -264,13 +265,13 @@ def flag_excludes_by_faction_presence(world: SC2World, item_list: List[FilterIte for item in item_list: # Catch-all for all of a faction's items - if (not terran_missions and item.data.race == SC2Race.TERRAN): + if (not terran_missions or SC2Race.TERRAN not in races) and item.data.race == SC2Race.TERRAN: item.flags |= ItemFilterFlags.Excluded continue - if (not zerg_missions and item.data.race == SC2Race.ZERG): + if (not zerg_missions or SC2Race.ZERG not in races) and item.data.race == SC2Race.ZERG: item.flags |= ItemFilterFlags.Excluded continue - if (not protoss_missions and item.data.race == SC2Race.PROTOSS): + if (not protoss_missions or SC2Race.PROTOSS not in races) and item.data.race == SC2Race.PROTOSS: if item.name not in item_groups.soa_items: item.flags |= ItemFilterFlags.Excluded continue diff --git a/worlds/sc2/client.py b/worlds/sc2/client.py index de50079def6a..dd107cadede1 100644 --- a/worlds/sc2/client.py +++ b/worlds/sc2/client.py @@ -1013,6 +1013,7 @@ def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool: return get_full_item_list()[item_names.KERRIGAN_PRIMAL_FORM].code in codes return False + async def starcraft_launch(ctx: SC2Context, mission_id: int): sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id].mission_name}. If game does not launch check log file for errors.") @@ -1084,7 +1085,7 @@ async def on_step(self, iteration: int): self.ctx.mission_order, 1 if self.ctx.nova_covert_ops_only else 0, self.ctx.grant_story_levels, - self.ctx.enable_morphling + self.ctx.enable_morphling, )) await self.chat_send("?GiveResources {} {} {}".format( start_items[SC2Race.ANY][0], diff --git a/worlds/sc2/mission_tables.py b/worlds/sc2/mission_tables.py index f047c18dccd2..68cb01e5d815 100644 --- a/worlds/sc2/mission_tables.py +++ b/worlds/sc2/mission_tables.py @@ -486,5 +486,5 @@ def get_campaign_potential_goal_missions(campaign: SC2Campaign) -> List[SC2Missi return missions -def get_no_build_missions() -> List[SC2Mission]: - return [mission for mission in SC2Mission if MissionFlag.NoBuild in mission.flags] +def get_missions_with_flags(flags: MissionFlag) -> List[SC2Mission]: + return [mission for mission in SC2Mission if flags & mission.flags] diff --git a/worlds/sc2/options.py b/worlds/sc2/options.py index cb061c241fdf..ec0fc2976ec2 100644 --- a/worlds/sc2/options.py +++ b/worlds/sc2/options.py @@ -6,8 +6,8 @@ PerGameCommonOptions, Option, VerifyKeys) from Utils import get_fuzzy_results from BaseClasses import PlandoOptions -from .mission_tables import SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_no_build_missions, \ - campaign_mission_table +from .mission_tables import SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_missions_with_flags, \ + campaign_mission_table, SC2Race, MissionFlag from .mission_orders import vanilla_shuffle_order, mini_campaign_order from .mission_groups import mission_groups, MissionGroupNames @@ -55,6 +55,23 @@ def __len__(self) -> int: return self.value.__len__() +class SelectRaces(Choice): + """ + Pick which factions' missions and items can be shuffled into the world. + """ + # bit 0: terran, bit 1: zerg, bit 2: protoss. all disabled means plando only + display_name = "Select Playable Races" + option_all = 7 + option_terran = 1 + option_zerg = 2 + option_protoss = 4 + option_terran_and_zerg = 3 + option_terran_and_protoss = 5 + option_zerg_and_protoss = 6 + option_plando = 0 + default = option_all + + class GameDifficulty(Choice): """ The difficulty of the campaign, affects enemy AI, starting units, and game speed. @@ -874,6 +891,7 @@ class StartingSupplyPerItem(Range): @dataclass class Starcraft2Options(PerGameCommonOptions): + selected_races: SelectRaces game_difficulty: GameDifficulty game_speed: GameSpeed disable_forced_camera: DisableForcedCamera @@ -944,6 +962,23 @@ def get_option_value(world: 'SC2World', name: str) -> Union[int, FrozenSet]: return player_option.value +def get_enabled_races(world: 'SC2World') -> Set[SC2Race]: + selection = get_option_value(world, 'selected_races') + if selection == SelectRaces.option_all: + return set(SC2Race) + enabled = set() + # if bit 0, enable terran + if selection & 1: + enabled.add(SC2Race.TERRAN) + # if bit 1, enable zerg + if selection & (1 << 1): + enabled.add(SC2Race.ZERG) + # if bit 2, enable protoss + if selection & (1 << 2): + enabled.add(SC2Race.PROTOSS) + return enabled + + def get_enabled_campaigns(world: 'SC2World') -> Set[SC2Campaign]: enabled_campaigns = set() if get_option_value(world, "enable_wol_missions"): @@ -960,7 +995,7 @@ def get_enabled_campaigns(world: 'SC2World') -> Set[SC2Campaign]: enabled_campaigns.add(SC2Campaign.EPILOGUE) if get_option_value(world, "enable_nco_missions"): enabled_campaigns.add(SC2Campaign.NCO) - return enabled_campaigns + return set([campaign for campaign in enabled_campaigns if campaign.race in get_enabled_races(world)]) def get_disabled_campaigns(world: 'SC2World') -> Set[SC2Campaign]: @@ -971,11 +1006,27 @@ def get_disabled_campaigns(world: 'SC2World') -> Set[SC2Campaign]: return disabled_campaigns +def get_disabled_flags(world: 'SC2World') -> MissionFlag: + excluded = 0 + races = get_enabled_races(world) + # filter out missions based on disabled races + if SC2Race.TERRAN not in races: + excluded |= MissionFlag.Terran + if SC2Race.ZERG not in races: + excluded |= MissionFlag.Zerg + if SC2Race.PROTOSS not in races: + excluded |= MissionFlag.Protoss + # filter out no-build missions + if not get_option_value(world, "shuffle_no_build"): + excluded |= MissionFlag.NoBuild + return MissionFlag(excluded) + + def get_excluded_missions(world: 'SC2World') -> Set[SC2Mission]: mission_order_type = world.options.mission_order.value excluded_mission_names = world.options.excluded_missions.value - shuffle_no_build = world.options.shuffle_no_build.value disabled_campaigns = get_disabled_campaigns(world) + disabled_flags = get_disabled_flags(world) excluded_missions: Set[SC2Mission] = set([lookup_name_to_mission[name] for name in excluded_mission_names]) @@ -991,8 +1042,8 @@ def get_excluded_missions(world: 'SC2World') -> Set[SC2Mission]: mission.pool == MissionPools.VERY_HARD and mission.campaign != SC2Campaign.EPILOGUE] ) # Omitting No-Build missions if not shuffling no-build - if not shuffle_no_build: - excluded_missions = excluded_missions.union(get_no_build_missions()) + if disabled_flags: + excluded_missions = excluded_missions.union(get_missions_with_flags(disabled_flags)) # Omitting missions not in enabled campaigns for campaign in disabled_campaigns: excluded_missions = excluded_missions.union(campaign_mission_table[campaign]) diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py index d4fa0b412226..b3148dd8e26f 100644 --- a/worlds/sc2/rules.py +++ b/worlds/sc2/rules.py @@ -340,9 +340,12 @@ def zerg_competent_anti_air(self, state: CollectionState) -> bool: or (self.advanced_tactics and state.has(item_names.INFESTOR, self.player)) def zerg_basic_anti_air(self, state: CollectionState) -> bool: - return self.zerg_competent_anti_air(state) or self.kerrigan_unit_available in kerrigan_unit_available or \ - state.has_any({item_names.SWARM_QUEEN, item_names.SCOURGE}, self.player) or (self.advanced_tactics and state.has(item_names.SPORE_CRAWLER, self.player)) - + return self.zerg_basic_kerriganless_anti_air(state) or self.kerrigan_unit_available in kerrigan_unit_available + + def zerg_basic_kerriganless_anti_air(self, state: CollectionState) -> bool: + return self.zerg_competent_anti_air(state) or state.has_any({item_names.SWARM_QUEEN, item_names.SCOURGE}, self.player) \ + or (self.advanced_tactics and state.has(item_names.SPORE_CRAWLER, self.player)) + def morph_brood_lord(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_BROOD_LORD_ASPECT, self.player) diff --git a/worlds/sc2/test/test_usecases.py b/worlds/sc2/test/test_usecases.py index b0028223bc06..07c64139c255 100644 --- a/worlds/sc2/test/test_usecases.py +++ b/worlds/sc2/test/test_usecases.py @@ -154,3 +154,24 @@ def test_resource_filler_items_may_be_put_in_start_inventory(self) -> None: self.assertEqual(start_item_names.count(item_names.STARTING_VESPENE), NUM_RESOURCE_ITEMS, "Wrong number of starting vespene in starting inventory") self.assertEqual(start_item_names.count(item_names.STARTING_SUPPLY), NUM_RESOURCE_ITEMS, "Wrong number of starting supply in starting inventory") + def test_excluding_race_excludes_campaigns_and_items(self) -> None: + # default options except vanilla shuffled and no terran; should generate without terran campaigns or items (or Epilogue, due to absent WoL) + world_options = { + 'selected_races': options.SelectRaces.option_zerg_and_protoss, + 'enable_wol_missions': True, + 'enable_nco_missions': True, + 'enable_prophecy_missions': True, + 'enable_hots_missions': True, + 'enable_lotv_prologue_missions': True, + 'enable_lotv_missions': True, + 'enable_epilogue_missions': True, + 'mission_order': options.MissionOrder.option_vanilla_shuffled, + } + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + world_regions = [region.name for region in self.multiworld.regions] + world_regions.remove('Menu') + for item_name in world_item_names: + self.assertNotEqual(items.item_table[item_name].race, mission_tables.SC2Race.TERRAN) + for region in world_regions: + self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign, (mission_tables.SC2Campaign.WOL, mission_tables.SC2Campaign.NCO, mission_tables.SC2Campaign.EPILOGUE))