diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index 983b329d7d..0df315c629 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -374,22 +374,32 @@ def create_and_flag_explicit_item_locks_and_excludes(world: SC2World) -> List[Fi Handles `excluded_items`, `locked_items`, and `start_inventory` Returns a list of all possible non-filler items that can be added, with an accompanying flags bitfield. """ - excluded_items = world.options.excluded_items - unexcluded_items = world.options.unexcluded_items - locked_items = world.options.locked_items - start_inventory = world.options.start_inventory + excluded_items: dict[str, int] = world.options.excluded_items.value + unexcluded_items: dict[str, int] = world.options.unexcluded_items.value + locked_items: dict[str, int] = world.options.locked_items.value + start_inventory: dict[str, int] = world.options.start_inventory.value key_items = world.custom_mission_order.get_items_to_lock() - def resolve_count(count: Optional[int], max_count: int) -> int: - if count == 0: + def resolve_exclude(count: int, max_count: int) -> int: + if count < 0: return max_count - if count is None: - return 0 - if max_count == 0: - return count - return min(count, max_count) + return count + + def resolve_count(count: int, max_count: int, negative_value: int | None = None) -> int: + """ + Handles `count` being out of range. + * If `count > max_count`, returns `max_count`. + * If `count < 0`, returns `negative_value` (returns `max_count` if `negative_value` is unspecified) + """ + if count < 0: + if negative_value is None: + return max_count + return negative_value + if max_count and count > max_count: + return max_count + return count - auto_excludes = {item_name: 1 for item_name in item_groups.legacy_items} + auto_excludes = Counter({item_name: 1 for item_name in item_groups.legacy_items}) if world.options.exclude_overpowered_items.value == ExcludeOverpoweredItems.option_true: for item_name in item_groups.overpowered_items: auto_excludes[item_name] = 1 @@ -402,28 +412,29 @@ def create_and_flag_explicit_item_locks_and_excludes(world: SC2World) -> List[Fi elif item_name in item_groups.nova_equipment: continue else: - auto_excludes[item_name] = 0 + auto_excludes[item_name] = item_data.quantity result: List[FilterItem] = [] for item_name, item_data in item_tables.item_table.items(): max_count = item_data.quantity - auto_excluded_count = auto_excludes.get(item_name) + auto_excluded_count = auto_excludes.get(item_name, 0) excluded_count = excluded_items.get(item_name, auto_excluded_count) - unexcluded_count = unexcluded_items.get(item_name) - locked_count = locked_items.get(item_name) - start_count: Optional[int] = start_inventory.get(item_name) + unexcluded_count = unexcluded_items.get(item_name, 0) + locked_count = locked_items.get(item_name, 0) + start_count = start_inventory.get(item_name, 0) key_count = key_items.get(item_name, 0) - # specifying 0 in the yaml means exclude / lock all - # start_inventory doesn't allow specifying 0 - # not specifying means don't exclude/lock/start - excluded_count = resolve_count(excluded_count, max_count) - unexcluded_count = resolve_count(unexcluded_count, max_count) + # Specifying a negative number in the yaml means exclude / lock / start all. + # In the case of excluded/unexcluded, resolve negatives to max_count before subtracting them, + # and after subtraction resolve negatives to just 0 (when unexcluded > excluded). + excluded_count = resolve_count( + resolve_exclude(excluded_count, max_count) - resolve_exclude(unexcluded_count, max_count), + max_count, + negative_value=0 + ) locked_count = resolve_count(locked_count, max_count) start_count = resolve_count(start_count, max_count) - excluded_count = max(0, excluded_count - unexcluded_count) - # Priority: start_inventory >> locked_items >> excluded_items >> unspecified if max_count == 0: if excluded_count: @@ -486,8 +497,9 @@ def flag_excludes_by_faction_presence(world: SC2World, item_list: List[FilterIte item.flags |= ItemFilterFlags.FilterExcluded continue if not zerg_missions and item.data.race == SC2Race.ZERG: - if item.data.type != item_tables.ZergItemType.Ability \ - and item.data.type != ZergItemType.Level: + if (item.data.type != item_tables.ZergItemType.Ability + and item.data.type != ZergItemType.Level + ): item.flags |= ItemFilterFlags.FilterExcluded continue if not protoss_missions and item.data.race == SC2Race.PROTOSS: @@ -641,7 +653,7 @@ def flag_mission_based_item_excludes(world: SC2World, item_list: List[FilterItem item.flags |= ItemFilterFlags.FilterExcluded # Remove Spear of Adun passives - if item.name in item_tables.spear_of_adun_castable_passives and not soa_passive_presence: + if item.name in item_groups.spear_of_adun_passives and not soa_passive_presence: item.flags |= ItemFilterFlags.FilterExcluded # Remove matchup-specific items if you don't play that matchup diff --git a/worlds/sc2/client.py b/worlds/sc2/client.py index 1722c031db..708b00cd27 100644 --- a/worlds/sc2/client.py +++ b/worlds/sc2/client.py @@ -40,6 +40,7 @@ from .options import ( SpearOfAdunPassivesPresentInNoBuild, EnableVoidTrade, VoidTradeAgeLimit, void_trade_age_limits_ms, VoidTradeWorkers, DifficultyDamageModifier, MissionOrderScouting, GenericUpgradeResearchSpeedup, MercenaryHighlanders, WarCouncilNerfs, is_mission_in_soa_presence, + upgrade_included_names, ) from .mission_order.slot_data import CampaignSlotData, LayoutSlotData, MissionSlotData, MissionOrderObjectSlotData from .mission_order.entry_rules import SubRuleRuleData, CountMissionsRuleData, MissionEntryRules @@ -71,10 +72,12 @@ from .mission_tables import ( ) import colorama -from .options import Option, upgrade_included_names from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes from MultiServer import mark_raw +if typing.TYPE_CHECKING: + from Options import Option + pool = concurrent.futures.ThreadPoolExecutor(1) loop = asyncio.get_event_loop_policy().new_event_loop() nest_asyncio.apply(loop) diff --git a/worlds/sc2/item/item_groups.py b/worlds/sc2/item/item_groups.py index ea65dc3e4a..021450cb73 100644 --- a/worlds/sc2/item/item_groups.py +++ b/worlds/sc2/item/item_groups.py @@ -167,6 +167,7 @@ class ItemGroupNames: LOTV_UNITS = "LotV Units" LOTV_ITEMS = "LotV Items" LOTV_GLOBAL_UPGRADES = "LotV Global Upgrades" + SOA_PASSIVES = "SOA Passive Abilities" SOA_ITEMS = "SOA" PROTOSS_GLOBAL_UPGRADES = "Protoss Global Upgrades" PROTOSS_BUILDINGS = "Protoss Buildings" @@ -777,11 +778,21 @@ item_name_groups[ItemGroupNames.PURIFIER_UNITS] = [ item_names.MIRAGE, item_names.DAWNBRINGER, item_names.TRIREME, item_names.TEMPEST, item_names.CALADRIUS, ] -item_name_groups[ItemGroupNames.SOA_ITEMS] = soa_items = [ +item_name_groups[ItemGroupNames.SOA_PASSIVES] = spear_of_adun_passives = [ + item_names.RECONSTRUCTION_BEAM, + item_names.OVERWATCH, + item_names.GUARDIAN_SHELL, +] +spear_of_adun_actives = [ *[item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Spear_Of_Adun], item_names.SOA_PROGRESSIVE_PROXY_PYLON, ] -lotv_soa_items = [item_name for item_name in soa_items if item_name != item_names.SOA_PYLON_OVERCHARGE] +item_name_groups[ItemGroupNames.SOA_ITEMS] = soa_items = spear_of_adun_actives + spear_of_adun_passives +lotv_soa_items = [ + item_name + for item_name in soa_items + if item_name not in (item_names.SOA_PYLON_OVERCHARGE, item_names.OVERWATCH) +] item_name_groups[ItemGroupNames.PROTOSS_GLOBAL_UPGRADES] = [ item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Solarite_Core ] diff --git a/worlds/sc2/item/item_tables.py b/worlds/sc2/item/item_tables.py index d63b00489f..d77971a721 100644 --- a/worlds/sc2/item/item_tables.py +++ b/worlds/sc2/item/item_tables.py @@ -2293,12 +2293,6 @@ spear_of_adun_calldowns = { item_names.SOA_SOLAR_BOMBARDMENT } -spear_of_adun_castable_passives = { - item_names.RECONSTRUCTION_BEAM, - item_names.OVERWATCH, - item_names.GUARDIAN_SHELL, -} - nova_equipment = { *[item_name for item_name, item_data in get_full_item_list().items() if item_data.type == TerranItemType.Nova_Gear], diff --git a/worlds/sc2/options.py b/worlds/sc2/options.py index d4274ab0c4..214a3194db 100644 --- a/worlds/sc2/options.py +++ b/worlds/sc2/options.py @@ -5,14 +5,13 @@ from datetime import timedelta from Options import ( Choice, Toggle, DefaultOnToggle, OptionSet, Range, - PerGameCommonOptions, Option, VerifyKeys, StartInventory, + PerGameCommonOptions, VerifyKeys, StartInventory, is_iterable_except_str, OptionGroup, Visibility, ItemDict, - Accessibility, ProgressionBalancing + OptionCounter, ) from Utils import get_fuzzy_results from BaseClasses import PlandoOptions -from .item import item_names, item_tables -from .item.item_groups import kerrigan_active_abilities, kerrigan_passives, nova_weapons, nova_gadgets +from .item import item_names, item_tables, item_groups from .mission_tables import ( SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_missions_with_any_flags_in_list, campaign_mission_table, SC2Race, MissionFlag @@ -700,7 +699,7 @@ class KerriganMaxActiveAbilities(Range): """ display_name = "Kerrigan Maximum Active Abilities" range_start = 0 - range_end = len(kerrigan_active_abilities) + range_end = len(item_groups.kerrigan_active_abilities) default = range_end @@ -711,7 +710,7 @@ class KerriganMaxPassiveAbilities(Range): """ display_name = "Kerrigan Maximum Passive Abilities" range_start = 0 - range_end = len(kerrigan_passives) + range_end = len(item_groups.kerrigan_passives) default = range_end @@ -829,7 +828,7 @@ class SpearOfAdunMaxAutocastAbilities(Range): """ display_name = "Spear of Adun Maximum Passive Abilities" range_start = 0 - range_end = sum(item.quantity for item_name, item in item_tables.get_full_item_list().items() if item_name in item_tables.spear_of_adun_castable_passives) + range_end = sum(item_tables.item_table[item_name].quantity for item_name in item_groups.spear_of_adun_passives) default = range_end @@ -883,7 +882,7 @@ class NovaMaxWeapons(Range): """ display_name = "Nova Maximum Weapons" range_start = 0 - range_end = len(nova_weapons) + range_end = len(item_groups.nova_weapons) default = range_end @@ -897,7 +896,7 @@ class NovaMaxGadgets(Range): """ display_name = "Nova Maximum Gadgets" range_start = 0 - range_end = len(nova_gadgets) + range_end = len(item_groups.nova_gadgets) default = range_end @@ -932,33 +931,48 @@ class TakeOverAIAllies(Toggle): display_name = "Take Over AI Allies" -class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]): - """A branch of ItemDict that supports item counts of 0""" +class Sc2ItemDict(OptionCounter, VerifyKeys, Mapping[str, int]): + """A branch of ItemDict that supports negative item counts""" default = {} supports_weighting = False verify_item_name = True # convert_name_groups = True display_name = 'Unnamed dictionary' - minimum_value: int = 0 + # Note(phaneros): Limiting minimum to -1 means that if two triggers add -1 to the same item, + # the validation fails. So give trigger people space to stack a bunch of triggers. + min: int = -1000 + max: int = 1000 + valid_keys = set(item_tables.item_table) | set(item_groups.item_name_groups) - def __init__(self, value: Dict[str, int]): + def __init__(self, value: dict[str, int]): self.value = {key: val for key, val in value.items()} @classmethod - def from_any(cls, data: Union[List[str], Dict[str, int]]) -> 'Sc2ItemDict': + def from_any(cls, data: list[str] | dict[str, int]) -> 'Sc2ItemDict': if isinstance(data, list): - # This is a little default that gets us backwards compatibility with lists. - # It doesn't play nice with trigger merging dicts and lists together, though, so best not to advertise it overmuch. - data = {item: 0 for item in data} + raise ValueError( + f"{cls.display_name}: Cannot convert from list. " + f"Use dict syntax (no dashes, 'value: number' synax)." + ) if isinstance(data, dict): for key, value in data.items(): if not isinstance(value, int): - raise ValueError(f"Invalid type in '{cls.display_name}': element '{key}' maps to '{value}', expected an integer") - if value < cls.minimum_value: - raise ValueError(f"Invalid value for '{cls.display_name}': element '{key}' maps to {value}, which is less than the minimum ({cls.minimum_value})") + raise ValueError( + f"Invalid type in '{cls.display_name}': " + f"element '{key}' maps to '{value}', expected an integer" + ) + if value < cls.min: + raise ValueError( + f"Invalid value for '{cls.display_name}': " + f"element '{key}' maps to {value}, which is less than the minimum ({cls.min})" + ) + if value > cls.max: + raise ValueError(f"Invalid value for '{cls.display_name}': " + f"element '{key}' maps to {value}, which is greater than the maximum ({cls.max})" + ) return cls(data) else: - raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") + raise NotImplementedError(f"{cls.display_name}: Cannot convert from non-dictionary, got {type(data)}") def verify(self, world: Type['World'], player_name: str, plando_options: PlandoOptions) -> None: """Overridden version of function from Options.VerifyKeys for a better error message""" @@ -974,15 +988,16 @@ class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]): self.value = new_value for item_name in self.value: if item_name not in world.item_names: - from .item import item_groups picks = get_fuzzy_results( item_name, list(world.item_names) + list(item_groups.ItemGroupNames.get_all_group_names()), limit=1, ) - raise Exception(f"Item {item_name} from option {self} " - f"is not a valid item name from {world.game}. " - f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") + raise Exception( + f"Item {item_name} from option {self} " + f"is not a valid item name from {world.game}. " + f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)" + ) def get_option_name(self, value): return ", ".join(f"{key}: {v}" for key, v in value.items()) @@ -998,25 +1013,25 @@ class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]): class Sc2StartInventory(Sc2ItemDict): - """Start with these items.""" + """Start with these items. Use an amount of -1 to start with all copies of an item.""" display_name = StartInventory.display_name class LockedItems(Sc2ItemDict): """Guarantees that these items will be unlockable, in the amount specified. - Specify an amount of 0 to lock all copies of an item.""" + Specify an amount of -1 to lock all copies of an item.""" display_name = "Locked Items" class ExcludedItems(Sc2ItemDict): """Guarantees that these items will not be unlockable, in the amount specified. - Specify an amount of 0 to exclude all copies of an item.""" + Specify an amount of -1 to exclude all copies of an item.""" display_name = "Excluded Items" class UnexcludedItems(Sc2ItemDict): """Undoes an item exclusion; useful for whitelisting or fine-tuning a category. - Specify an amount of 0 to unexclude all copies of an item.""" + Specify an amount of -1 to unexclude all copies of an item.""" display_name = "Unexcluded Items" diff --git a/worlds/sc2/pool_filter.py b/worlds/sc2/pool_filter.py index 31e47934ee..0ffa08e010 100644 --- a/worlds/sc2/pool_filter.py +++ b/worlds/sc2/pool_filter.py @@ -3,8 +3,7 @@ from typing import Callable, Dict, List, Set, Tuple, TYPE_CHECKING, Iterable from BaseClasses import Location, ItemClassification from .item import StarcraftItem, ItemFilterFlags, item_names, item_parents, item_groups -from .item.item_tables import item_table, TerranItemType, ZergItemType, spear_of_adun_calldowns, \ - spear_of_adun_castable_passives +from .item.item_tables import item_table, TerranItemType, ZergItemType, spear_of_adun_calldowns from .options import RequiredTactics if TYPE_CHECKING: @@ -272,7 +271,7 @@ class ValidInventory: self.world.random.shuffle(spear_of_adun_actives) cull_items_over_maximum(spear_of_adun_actives, self.world.options.spear_of_adun_max_active_abilities.value) - spear_of_adun_autocasts = [item for item in inventory if item.name in spear_of_adun_castable_passives] + spear_of_adun_autocasts = [item for item in inventory if item.name in item_groups.spear_of_adun_passives] self.world.random.shuffle(spear_of_adun_autocasts) cull_items_over_maximum(spear_of_adun_autocasts, self.world.options.spear_of_adun_max_passive_abilities.value) diff --git a/worlds/sc2/test/test_generation.py b/worlds/sc2/test/test_generation.py index 9a95058720..329cd593e1 100644 --- a/worlds/sc2/test/test_generation.py +++ b/worlds/sc2/test/test_generation.py @@ -18,19 +18,19 @@ class TestItemFiltering(Sc2SetupTestBase): world_options = { **self.ALL_CAMPAIGNS, 'locked_items': { - item_names.MARINE: 0, - item_names.MARAUDER: 0, + item_names.MARINE: -1, + item_names.MARAUDER: -1, item_names.MEDIVAC: 1, item_names.FIREBAT: 1, - item_names.ZEALOT: 0, + item_names.ZEALOT: -1, item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2, }, 'excluded_items': { - item_names.MARINE: 0, - item_names.MARAUDER: 0, - item_names.MEDIVAC: 0, + item_names.MARINE: -1, + item_names.MARAUDER: -1, + item_names.MEDIVAC: -1, item_names.FIREBAT: 1, - item_names.ZERGLING: 0, + item_names.ZERGLING: -1, item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2, } } @@ -50,38 +50,38 @@ class TestItemFiltering(Sc2SetupTestBase): world_options = { 'grant_story_tech': options.GrantStoryTech.option_grant, 'excluded_items': { - item_groups.ItemGroupNames.NOVA_EQUIPMENT: 15, + item_groups.ItemGroupNames.NOVA_EQUIPMENT: -1, item_names.MARINE_PROGRESSIVE_STIMPACK: 1, item_names.MARAUDER_PROGRESSIVE_STIMPACK: 2, - item_names.MARINE: 0, - item_names.MARAUDER: 0, + item_names.MARINE: -1, + item_names.MARAUDER: -1, item_names.REAPER: 1, - item_names.DIAMONDBACK: 0, + item_names.DIAMONDBACK: -1, item_names.HELLION: 1, # Additional excludes to increase the likelihood that unexcluded items actually appear - item_groups.ItemGroupNames.STARPORT_UNITS: 0, - item_names.WARHOUND: 0, - item_names.VULTURE: 0, - item_names.WIDOW_MINE: 0, - item_names.THOR: 0, - item_names.GHOST: 0, - item_names.SPECTRE: 0, - item_groups.ItemGroupNames.MENGSK_UNITS: 0, - item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: 0, + item_groups.ItemGroupNames.STARPORT_UNITS: -1, + item_names.WARHOUND: -1, + item_names.VULTURE: -1, + item_names.WIDOW_MINE: -1, + item_names.THOR: -1, + item_names.GHOST: -1, + item_names.SPECTRE: -1, + item_groups.ItemGroupNames.MENGSK_UNITS: -1, + item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: -1, }, 'unexcluded_items': { - item_names.NOVA_PLASMA_RIFLE: 1, # Necessary to pass logic - item_names.NOVA_PULSE_GRENADES: 0, # Necessary to pass logic - item_names.NOVA_JUMP_SUIT_MODULE: 0, # Necessary to pass logic - item_groups.ItemGroupNames.BARRACKS_UNITS: 0, + item_names.NOVA_PLASMA_RIFLE: 1, # Necessary to pass logic + item_names.NOVA_PULSE_GRENADES: -1, # Necessary to pass logic + item_names.NOVA_JUMP_SUIT_MODULE: -1, # Necessary to pass logic + item_groups.ItemGroupNames.BARRACKS_UNITS: -1, item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: 1, item_names.HELLION: 1, item_names.MARINE_PROGRESSIVE_STIMPACK: 1, - item_names.MARAUDER_PROGRESSIVE_STIMPACK: 0, + item_names.MARAUDER_PROGRESSIVE_STIMPACK: -1, # Additional unexcludes for logic - item_names.MEDIVAC: 0, - item_names.BATTLECRUISER: 0, - item_names.SCIENCE_VESSEL: 0, + item_names.MEDIVAC: -1, + item_names.BATTLECRUISER: -1, + item_names.SCIENCE_VESSEL: -1, }, # Terran-only 'enabled_campaigns': { @@ -103,11 +103,29 @@ class TestItemFiltering(Sc2SetupTestBase): self.assertNotIn(item_names.NOVA_BLAZEFIRE_GUNBLADE, itempool) self.assertNotIn(item_names.NOVA_ENERGY_SUIT_MODULE, itempool) + def test_exclude_2_beats_unexclude_1(self) -> None: + world_options = { + options.OPTION_NAME[options.ExcludedItems]: { + item_names.MARINE: 2, + }, + options.OPTION_NAME[options.UnexcludedItems]: { + item_names.MARINE: 1, + }, + # Ensure enough locations that marine doesn't get culled + options.OPTION_NAME[options.SelectedRaces]: { + SC2Race.TERRAN.get_title(), + }, + options.OPTION_NAME[options.VictoryCache]: 9, + } + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + self.assertNotIn(item_names.MARINE, itempool) + def test_excluding_groups_excludes_all_items_in_group(self): world_options = { - 'excluded_items': [ - item_groups.ItemGroupNames.BARRACKS_UNITS.lower(), - ] + 'excluded_items': { + item_groups.ItemGroupNames.BARRACKS_UNITS.lower(): -1, + }, } self.generate_world(world_options) itempool = [item.name for item in self.multiworld.itempool] @@ -337,9 +355,9 @@ class TestItemFiltering(Sc2SetupTestBase): # Options under test 'vanilla_items_only': True, 'unexcluded_items': { - item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: 0, + item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: -1, item_names.WARHOUND: 1, - item_groups.ItemGroupNames.TERRAN_STIMPACKS: 0, + item_groups.ItemGroupNames.TERRAN_STIMPACKS: -1, }, # Avoid options that lock non-vanilla items for logic 'required_tactics': options.RequiredTactics.option_any_units, @@ -463,12 +481,12 @@ class TestItemFiltering(Sc2SetupTestBase): }, 'required_tactics': options.RequiredTactics.option_no_logic, 'enable_morphling': options.EnableMorphling.option_true, - 'excluded_items': [ - item_groups.ItemGroupNames.ZERG_UNITS.lower() - ], - 'unexcluded_items': [ - item_groups.ItemGroupNames.ZERG_MORPHS.lower() - ] + 'excluded_items': { + item_groups.ItemGroupNames.ZERG_UNITS.lower(): -1, + }, + 'unexcluded_items': { + item_groups.ItemGroupNames.ZERG_MORPHS.lower(): -1, + }, } self.generate_world(world_options) itempool = [item.name for item in self.multiworld.itempool] @@ -486,12 +504,12 @@ class TestItemFiltering(Sc2SetupTestBase): }, 'required_tactics': options.RequiredTactics.option_no_logic, 'enable_morphling': options.EnableMorphling.option_false, - 'excluded_items': [ - item_groups.ItemGroupNames.ZERG_UNITS.lower() - ], - 'unexcluded_items': [ - item_groups.ItemGroupNames.ZERG_MORPHS.lower() - ] + 'excluded_items': { + item_groups.ItemGroupNames.ZERG_UNITS.lower(): -1, + }, + 'unexcluded_items': { + item_groups.ItemGroupNames.ZERG_MORPHS.lower(): -1, + }, } self.generate_world(world_options) itempool = [item.name for item in self.multiworld.itempool] @@ -520,14 +538,14 @@ class TestItemFiltering(Sc2SetupTestBase): def test_planetary_orbital_module_not_present_without_cc_spells(self) -> None: world_options = { - "excluded_items": [ - item_names.COMMAND_CENTER_MULE, - item_names.COMMAND_CENTER_SCANNER_SWEEP, - item_names.COMMAND_CENTER_EXTRA_SUPPLIES - ], - "locked_items": [ - item_names.PLANETARY_FORTRESS - ] + "excluded_items": { + item_names.COMMAND_CENTER_MULE: -1, + item_names.COMMAND_CENTER_SCANNER_SWEEP: -1, + item_names.COMMAND_CENTER_EXTRA_SUPPLIES: -1, + }, + "locked_items": { + item_names.PLANETARY_FORTRESS: -1, + } } self.generate_world(world_options) @@ -931,10 +949,10 @@ class TestItemFiltering(Sc2SetupTestBase): } }, 'grant_story_levels': options.GrantStoryLevels.option_additive, - 'excluded_items': [ - item_names.KERRIGAN_LEAPING_STRIKE, - item_names.KERRIGAN_MEND, - ] + 'excluded_items': { + item_names.KERRIGAN_LEAPING_STRIKE: -1, + item_names.KERRIGAN_MEND: -1, + } } self.generate_world(world_options) itempool = [item.name for item in self.multiworld.itempool] @@ -1208,7 +1226,7 @@ class TestItemFiltering(Sc2SetupTestBase): 'mission_order': MissionOrder.option_grid, 'maximum_campaign_size': MaximumCampaignSize.range_end, 'exclude_overpowered_items': ExcludeOverpoweredItems.option_true, - 'locked_items': [locked_item], + 'locked_items': {locked_item: -1}, 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, 'selected_races': [SC2Race.TERRAN.get_title()], } @@ -1249,7 +1267,7 @@ class TestItemFiltering(Sc2SetupTestBase): 'maximum_campaign_size': MaximumCampaignSize.range_end, 'exclude_overpowered_items': ExcludeOverpoweredItems.option_false, 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, - 'locked_items': {item_name: 0 for item_name in unreleased_items}, + 'locked_items': {item_name: -1 for item_name in unreleased_items}, } self.generate_world(world_options) @@ -1264,7 +1282,7 @@ class TestItemFiltering(Sc2SetupTestBase): **self.ALL_CAMPAIGNS, 'mission_order': MissionOrder.option_grid, 'maximum_campaign_size': MaximumCampaignSize.range_end, - 'excluded_items': [item_name for item_name in item_groups.terran_mercenaries], + 'excluded_items': {item_name: -1 for item_name in item_groups.terran_mercenaries}, 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, 'selected_races': [SC2Race.TERRAN.get_title()], } @@ -1280,7 +1298,7 @@ class TestItemFiltering(Sc2SetupTestBase): 'mission_order': MissionOrder.option_grid, 'maximum_campaign_size': MaximumCampaignSize.range_end, 'exclude_overpowered_items': ExcludeOverpoweredItems.option_true, - 'unexcluded_items': [item_names.SOA_TIME_STOP], + 'unexcluded_items': {item_names.SOA_TIME_STOP: -1}, 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, } @@ -1322,7 +1340,7 @@ class TestItemFiltering(Sc2SetupTestBase): 'enabled_campaigns': { SC2Campaign.WOL.campaign_name }, - 'excluded_items': [item_names.MARINE, item_names.MEDIC], + 'excluded_items': {item_names.MARINE: -1, item_names.MEDIC: -1}, 'shuffle_no_build': False, 'required_tactics': RequiredTactics.option_standard } diff --git a/worlds/sc2/test/test_item_filtering.py b/worlds/sc2/test/test_item_filtering.py index 7f8251c52a..bebd29cf6a 100644 --- a/worlds/sc2/test/test_item_filtering.py +++ b/worlds/sc2/test/test_item_filtering.py @@ -11,7 +11,7 @@ class ItemFilterTests(Sc2SetupTestBase): def test_excluding_all_barracks_units_excludes_infantry_upgrades(self) -> None: world_options = { 'excluded_items': { - item_groups.ItemGroupNames.BARRACKS_UNITS: 0 + item_groups.ItemGroupNames.BARRACKS_UNITS: -1, }, 'required_tactics': 'standard', 'min_number_of_upgrades': 1, diff --git a/worlds/sc2/test/test_usecases.py b/worlds/sc2/test/test_usecases.py index bf79dbea01..08d0cb4cc8 100644 --- a/worlds/sc2/test/test_usecases.py +++ b/worlds/sc2/test/test_usecases.py @@ -35,10 +35,10 @@ class TestSupportedUseCases(Sc2SetupTestBase): SC2Campaign.NCO.campaign_name }, 'excluded_items': { - item_groups.ItemGroupNames.TERRAN_UNITS: 0, + item_groups.ItemGroupNames.TERRAN_UNITS: -1, }, 'unexcluded_items': { - item_groups.ItemGroupNames.NCO_UNITS: 0, + item_groups.ItemGroupNames.NCO_UNITS: -1, }, 'max_number_of_upgrades': 2, } @@ -81,10 +81,10 @@ class TestSupportedUseCases(Sc2SetupTestBase): }, 'mission_order': options.MissionOrder.option_vanilla_shuffled, 'excluded_items': { - item_groups.ItemGroupNames.TERRAN_ITEMS: 0, + item_groups.ItemGroupNames.TERRAN_ITEMS: -1, }, 'unexcluded_items': { - item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: 0, + item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: -1, item_groups.ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS: 1, }, 'excluded_missions': [ @@ -398,7 +398,7 @@ class TestSupportedUseCases(Sc2SetupTestBase): self.generate_world(world_options) world_item_names = [item.name for item in self.multiworld.itempool] - spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_calldowns] + spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_groups.spear_of_adun_actives] self.assertLessEqual(len(spear_of_adun_actives), target_number) @@ -418,7 +418,9 @@ class TestSupportedUseCases(Sc2SetupTestBase): self.generate_world(world_options) world_item_names = [item.name for item in self.multiworld.itempool] - spear_of_adun_autocasts = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_castable_passives] + spear_of_adun_autocasts = [ + item_name for item_name in world_item_names if item_name in item_groups.spear_of_adun_passives + ] self.assertLessEqual(len(spear_of_adun_autocasts), target_number) @@ -471,12 +473,12 @@ class TestSupportedUseCases(Sc2SetupTestBase): ], 'required_tactics': options.RequiredTactics.option_any_units, 'excluded_items': { - item_groups.ItemGroupNames.TERRAN_UNITS: 0, - item_groups.ItemGroupNames.ZERG_UNITS: 0, + item_groups.ItemGroupNames.TERRAN_UNITS: -1, + item_groups.ItemGroupNames.ZERG_UNITS: -1, }, 'unexcluded_items': { - item_groups.ItemGroupNames.TERRAN_MERCENARIES: 0, - item_groups.ItemGroupNames.ZERG_MERCENARIES: 0, + item_groups.ItemGroupNames.TERRAN_MERCENARIES: -1, + item_groups.ItemGroupNames.ZERG_MERCENARIES: -1, }, 'start_inventory': { item_names.PROGRESSIVE_FAST_DELIVERY: 1,