diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py index efd0872527..d1921de209 100644 --- a/worlds/sc2wol/Options.py +++ b/worlds/sc2wol/Options.py @@ -38,22 +38,29 @@ class AllInMap(Choice): class MissionOrder(Choice): """Determines the order the missions are played in. Vanilla: Keeps the standard mission order and branching from the WoL Campaign. - Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.""" + Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within. + Mini Shuffle: Halves the number of missions in the campaign and randomizes the missions available. + Gauntlet: A linear series of 7 random missions to complete the campaign. + """ display_name = "Mission Order" option_vanilla = 0 option_vanilla_shuffled = 1 + option_mini_shuffle = 2 + option_gauntlet = 3 class ShuffleProtoss(DefaultOnToggle): - """Determines if the 3 protoss missions are included in the shuffle if Vanilla Shuffled is enabled. If this is - not the 3 protoss missions will stay in their vanilla order in the mission order making them optional to complete - the game.""" + """Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled. + On Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain if not shuffled. + On Mini Shuffle or Gauntlet, the 3 protoss missions will not appear and Protoss units are removed from the pool if not shuffled. + """ display_name = "Shuffle Protoss Missions" class RelegateNoBuildMissions(DefaultOnToggle): - """If enabled, all no build missions besides the needed first one will be placed at the end of optional routes so - that none of them become required to complete the game. Only takes effect if mission order is not set to vanilla.""" + """Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled. + On Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be placed at the end of optional routes. + On Mini Shuffle or Gauntlet, the 5 no-build missions will not appear.""" display_name = "Relegate No-Build Missions" diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py index f8540e3bcd..898975c5ab 100644 --- a/worlds/sc2wol/PoolFilter.py +++ b/worlds/sc2wol/PoolFilter.py @@ -1,6 +1,8 @@ -from BaseClasses import MultiWorld, Item, ItemClassification +from typing import Callable +from BaseClasses import MultiWorld, ItemClassification, Item, Location from .Items import item_table -from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list, vanilla_mission_req_table, starting_mission_locations +from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\ + mission_orders, starting_mission_locations, MissionInfo from .Options import get_option_value from .LogicMixin import SC2WoLLogic @@ -22,32 +24,39 @@ MIN_UNITS_PER_STRUCTURE = [ 0 # Gauntlet ] -PROTOSS_REGIONS = ["A Sinister Turn", "Echoes of the Future", "In Utter Darkness"] +PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"} -UPGRADES = [ - "Progressive Infantry Weapon", "Progressive Infantry Armor", - "Progressive Vehicle Weapon", "Progressive Vehicle Armor", - "Progressive Ship Weapon", "Progressive Ship Armor" +ALWAYS_USEFUL_ARMORY = [ + "Combat Shield (Marine)", "Stabilizer Medpacks (Medic)" # Needed for no-build logic ] -def filter_missions(world: MultiWorld, player: int) -> set[str]: +def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]: """ - Returns a semi-randomly pruned set of missions based on yaml configuration + Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets """ - missions = set(vanilla_mission_req_table.keys()) - mission_order = get_option_value(world, player, "mission_order") + + mission_order_type = get_option_value(world, player, "mission_order") shuffle_protoss = get_option_value(world, player, "shuffle_protoss") relegate_no_build = get_option_value(world, player, "relegate_no_build") # Vanilla and Vanilla Shuffled use the entire mission pool - if mission_order not in (2, 3): - return missions + if mission_order_type in (0, 1): + return { + 'no_build': no_build_regions_list[:], + 'easy': easy_regions_list[:], + 'medium': medium_regions_list[:], + 'hard': hard_regions_list[:] + } + + mission_count = 0 + for mission in mission_orders[mission_order_type]: + if mission is None: + continue + if mission.type == 'all_in': # All-In is placed separately + continue + mission_count += 1 - if mission_order == 2: - mission_count = 15 - else: - mission_count = 7 mission_sets = [ set(no_build_regions_list), set(easy_regions_list), @@ -56,12 +65,10 @@ def filter_missions(world: MultiWorld, player: int) -> set[str]: ] # Omitting Protoss missions if not shuffling protoss if not shuffle_protoss: - missions.difference_update(PROTOSS_REGIONS) for mission_set in mission_sets: mission_set.difference_update(PROTOSS_REGIONS) # Omitting No Build missions if relegating no-build if relegate_no_build: - missions.difference_update(no_build_regions_list) # The build missions in starting_mission_locations become the new "no build missions" mission_sets[0] = set(starting_mission_locations.keys()) mission_sets[0].difference_update(no_build_regions_list) @@ -70,124 +77,136 @@ def filter_missions(world: MultiWorld, player: int) -> set[str]: mission_set.difference_update(mission_sets[0]) # Removing random missions from each difficulty set in a cycle set_cycle = 0 - while len(missions) > mission_count: + mission_pools = [list(mission_set) for mission_set in mission_sets] + current_count = sum(len(mission_pool) for mission_pool in mission_pools) + if current_count < mission_count: + raise Exception('Not enough missions available to fill the campaign on current settings.') + while current_count > mission_count: if set_cycle == 4: set_cycle = 0 # Must contain at least one mission per set - if len(mission_sets[set_cycle]) == 1: - continue - removed_mission = world.random.choice(mission_sets[set_cycle]) - mission_sets[set_cycle].remove(removed_mission) - missions.remove(removed_mission) + mission_pool = mission_pools[set_cycle] set_cycle += 1 - return missions + if len(mission_pool) == 1: + continue + mission_pool.remove(world.random.choice(mission_pool)) + current_count -= 1 + return { + 'no_build': mission_pools[0], + 'easy': mission_pools[1], + 'medium': mission_pools[2], + 'hard': mission_pools[3] + } -class ValidInventory(SC2WoLLogic): +class ValidInventory: - def has(self, item: str, player: int = 0): + def has(self, item: str, player: int): return item in self.logical_inventory - def has_any(self, items: set[str], player: int = 0): - return any([item in self.logical_inventory for item in items]) + def has_any(self, items: set[str], player: int): + return any(item in self.logical_inventory for item in items) - # Necessary for Piercing the Shroud - def _sc2wol_has_mm_upgrade(self, world: MultiWorld, player: int): - return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player) + def has_all(self, items: set[str], player: int): + return all(item in self.logical_inventory for item in items) - # Necessary for Maw of the Void - def _sc2wol_survives_rip_field(self, world: MultiWorld, player: int): - return self.has("Battlecruiser", player) or \ - self._sc2wol_has_air(world, player) and \ - self._sc2wol_has_competent_anti_air(world, player) and \ - self.has("Science Vessel", player) + def has_units_per_structure(self, min_units_per_structure) -> bool: + return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > min_units_per_structure and \ + len(FACTORY_UNITS.intersection(self.logical_inventory)) > min_units_per_structure and \ + len(STARPORT_UNITS.intersection(self.logical_inventory)) > min_units_per_structure - def _sc2wol_has_units_per_structure(self, world: MultiWorld, player: int): - return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \ - len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \ - len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure - - def generate_reduced_inventory(self, number_of_locations: int): - inventory = set(self.inventory) + def generate_reduced_inventory(self, inventory_size: int, mission_requirements: list[Callable]) -> list[Item]: + inventory = list(self.item_pool) locked_items = list(self.locked_items) - self.logical_inventory = set(self.progression_items) - while len(inventory) + len(locked_items) > number_of_locations: + self.logical_inventory = {item.name for item in self.progression_items.union(self.locked_items)} + requirements = mission_requirements + mission_order_type = get_option_value(self.world, self.player, "mission_order") + min_units_per_structure = MIN_UNITS_PER_STRUCTURE[mission_order_type] + if min_units_per_structure > 0: + requirements.append(lambda state: state.has_units_per_structure(min_units_per_structure)) + while len(inventory) + len(locked_items) > inventory_size: if len(inventory) == 0: raise Exception('Reduced item pool generation failed.') # Select random item from removable items item = self.world.random.choice(inventory) inventory.remove(item) # Only run logic checks when removing logic items - if item in self.logical_inventory: - self.logical_inventory.remove(item) - if not all(self.requirements): - # If item cannot be removed, move it to locked items - self.logical_inventory.add(item) - locked_items.add(item) - else: + if item.name in self.logical_inventory: + self.logical_inventory.remove(item.name) + if all(requirement(self) for requirement in requirements): # If item can be removed and is a unit, remove armory upgrades - if item in UPGRADABLE_ITEMS: - inventory = [inv_item for inv_item in inventory if not inv_item.endswith('(' + item + ')')] - return list(inventory) + locked_items + # Some armory upgrades are kept regardless, as they remain logically relevant + if item.name in UPGRADABLE_ITEMS: + inventory = [ + inv_item for inv_item in inventory + if inv_item.name in ALWAYS_USEFUL_ARMORY or not inv_item.name.endswith('(' + item.name + ')') + ] + else: + # If item cannot be removed, move it to locked items + self.logical_inventory.add(item.name) + locked_items.append(item) + return inventory + locked_items - def __init__(self, world: MultiWorld, player: int, locked_items: list[str]): + def _read_logic(self): + self._sc2wol_has_common_unit = lambda world, player: SC2WoLLogic._sc2wol_has_common_unit(self, world, player) + self._sc2wol_has_air = lambda world, player: SC2WoLLogic._sc2wol_has_air(self, world, player) + self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player) + self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player) + self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player) + self._sc2wol_has_heavy_defense = lambda world, player: SC2WoLLogic._sc2wol_has_heavy_defense(self, world, player) + self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player) + self._sc2wol_has_train_killers = lambda world, player: SC2WoLLogic._sc2wol_has_train_killers(self, world, player) + self._sc2wol_able_to_rescue = lambda world, player: SC2WoLLogic._sc2wol_able_to_rescue(self, world, player) + self._sc2wol_beats_protoss_deathball = lambda world, player: SC2WoLLogic._sc2wol_beats_protoss_deathball(self, world, player) + self._sc2wol_survives_rip_field = lambda world, player: SC2WoLLogic._sc2wol_survives_rip_field(self, world, player) + self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player) + self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player) + self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player) + self._sc2wol_has_bunker_unit = lambda world, player: SC2WoLLogic._sc2wol_has_bunker_unit(self, world, player) + + def __init__(self, world: MultiWorld, player: int, + item_pool: list[Item], existing_items: list[Item], locked_items: list[Item], + has_protoss: bool): self.world = world - mission_order = get_option_value(world, player, "mission_order") - self.min_units_per_structure = MIN_UNITS_PER_STRUCTURE[mission_order] - self.locked_items = locked_items - self.inventory = set(item_table.keys()) - protoss_items = set() - # Scanning item table + self.player = player self.logical_inventory = set() self.progression_items = set() - for item_name, item in item_table.items(): + self.locked_items = locked_items[:] + self.existing_items = existing_items + self._read_logic() + # Initial filter of item pool + self.item_pool = [] + item_quantities: dict[str, int] = dict() + for item in item_pool: + item_info = item_table[item.name] if item.classification == ItemClassification.progression: - self.progression_items.add(item_name) - elif item.type in ("Minerals", "Vespene", "Supply"): - self.inventory.remove(item_name) - if item.type == "Protoss": - protoss_items.add(item_name) - self.requirements = [ - self._sc2wol_has_common_unit, - self._sc2wol_has_air, - self._sc2wol_has_competent_anti_air, - self._sc2wol_has_heavy_defense, - self._sc2wol_has_competent_comp, - self._sc2wol_has_train_killers, - self._sc2wol_able_to_rescue, - self._sc2wol_beats_protoss_deathball, - self._sc2wol_survives_rip_field - ] - # Only include units per structure requirement if more than 0 - if self.min_units_per_structure > 0: - self.requirements.append(self._sc2wol_has_units_per_structure) - # Only include Marine/Medic upgrade requirements if no-build missions are present - if mission_order in (1, 2) or get_option_value(world, player, "relegate_no_build"): - self.requirements.append(self._has_mm_upgrade) - # Only include protoss requirements if protoss missions are present - if mission_order in (1, 2) or get_option_value(world, player, "shuffle_protoss"): - self.requirements += [ - self._sc2wol_has_protoss_common_units, - self._sc2wol_has_protoss_medium_units - ] - else: - # Remove protoss items if protoss missions are not present - self.locked_items = [item for item in self.locked_items if item not in protoss_items] - self.inventory = [item for item in self.inventory if item not in protoss_items] - # 2 tiers of each upgrade are guaranteed - self.locked_items += 2 * UPGRADES - self.inventory.difference_update(locked_items) - # 1 tier of each upgrade can be potentially removed - self.inventory += UPGRADES + self.progression_items.add(item) + if item_info.type == 'Upgrade': + # All Upgrades are locked except for the final tier + if item.name not in item_quantities: + item_quantities[item.name] = 0 + item_quantities[item.name] += 1 + if item_quantities[item.name] < item_info.quantity: + self.locked_items.append(item) + else: + self.item_pool.append(item) + elif item_info.type == 'Goal': + locked_items.append(item) + elif item_info.type != 'Protoss' or has_protoss: + self.item_pool.append(item) -def filter_items(world: MultiWorld, player: int, regions: set[str], locked_items: list[str] = []) -> list[str]: +def filter_items(world: MultiWorld, player: int, mission_req_table: dict[str, MissionInfo], location_cache: list[Location], + item_pool: list[Item], existing_items: list[Item], locked_items: list[Item]) -> list[Item]: """ Returns a semi-randomly pruned set of items based on number of available locations. The returned inventory must be capable of logically accessing every location in the world. """ - valid_inventory = ValidInventory(world, player, locked_items) - number_of_locations = sum([ - info.extra_locations for name, info in vanilla_mission_req_table.items() if name in regions - ]) - return valid_inventory.generate_reduced_inventory(number_of_locations) \ No newline at end of file + open_locations = [location for location in location_cache if location.item is None] + inventory_size = len(open_locations) + has_protoss = bool(PROTOSS_REGIONS.intersection(mission_req_table.keys())) + mission_requirements = [location.access_rule for location in location_cache] + valid_inventory = ValidInventory(world, player, item_pool, existing_items, locked_items, has_protoss) + + valid_items = valid_inventory.generate_reduced_inventory(inventory_size, mission_requirements) + return valid_items diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index 8219a982c9..75abc1b530 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -2,55 +2,34 @@ from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType from .Locations import LocationData from .Options import get_option_value -from .MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \ - no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list +from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table +from .PoolFilter import filter_missions import random def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location]): locations_per_region = get_locations_per_region(locations) - regions = [ - create_region(world, player, locations_per_region, location_cache, "Menu"), - create_region(world, player, locations_per_region, location_cache, "Liberation Day"), - create_region(world, player, locations_per_region, location_cache, "The Outlaws"), - create_region(world, player, locations_per_region, location_cache, "Zero Hour"), - create_region(world, player, locations_per_region, location_cache, "Evacuation"), - create_region(world, player, locations_per_region, location_cache, "Outbreak"), - create_region(world, player, locations_per_region, location_cache, "Safe Haven"), - create_region(world, player, locations_per_region, location_cache, "Haven's Fall"), - create_region(world, player, locations_per_region, location_cache, "Smash and Grab"), - create_region(world, player, locations_per_region, location_cache, "The Dig"), - create_region(world, player, locations_per_region, location_cache, "The Moebius Factor"), - create_region(world, player, locations_per_region, location_cache, "Supernova"), - create_region(world, player, locations_per_region, location_cache, "Maw of the Void"), - create_region(world, player, locations_per_region, location_cache, "Devil's Playground"), - create_region(world, player, locations_per_region, location_cache, "Welcome to the Jungle"), - create_region(world, player, locations_per_region, location_cache, "Breakout"), - create_region(world, player, locations_per_region, location_cache, "Ghost of a Chance"), - create_region(world, player, locations_per_region, location_cache, "The Great Train Robbery"), - create_region(world, player, locations_per_region, location_cache, "Cutthroat"), - create_region(world, player, locations_per_region, location_cache, "Engine of Destruction"), - create_region(world, player, locations_per_region, location_cache, "Media Blitz"), - create_region(world, player, locations_per_region, location_cache, "Piercing the Shroud"), - create_region(world, player, locations_per_region, location_cache, "Whispers of Doom"), - create_region(world, player, locations_per_region, location_cache, "A Sinister Turn"), - create_region(world, player, locations_per_region, location_cache, "Echoes of the Future"), - create_region(world, player, locations_per_region, location_cache, "In Utter Darkness"), - create_region(world, player, locations_per_region, location_cache, "Gates of Hell"), - create_region(world, player, locations_per_region, location_cache, "Belly of the Beast"), - create_region(world, player, locations_per_region, location_cache, "Shatter the Sky"), - create_region(world, player, locations_per_region, location_cache, "All-In") - ] + mission_order_type = get_option_value(world, player, "mission_order") + mission_order = mission_orders[mission_order_type] + + mission_pools = filter_missions(world, player) + + used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool] + used_regions += ['All-In'] + regions = [create_region(world, player, locations_per_region, location_cache, "Menu")] + for region_name in used_regions: + regions.append(create_region(world, player, locations_per_region, location_cache, region_name)) if __debug__: - throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) + if mission_order_type in (0, 1): + throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) world.regions += regions names: Dict[str, int] = {} - if get_option_value(world, player, "mission_order") == 0: + if mission_order_type == 0: connect(world, player, names, 'Menu', 'Liberation Day'), connect(world, player, names, 'Liberation Day', 'The Outlaws', lambda state: state.has("Beat Liberation Day", player)), @@ -121,24 +100,22 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData return vanilla_mission_req_table - elif get_option_value(world, player, "mission_order") == 1: + else: missions = [] - no_build_pool = no_build_regions_list[:] - easy_pool = easy_regions_list[:] - medium_pool = medium_regions_list[:] - hard_pool = hard_regions_list[:] # Initial fill out of mission list and marking all-in mission - for mission in vanilla_shuffle_order: - if mission.type == "all_in": + for mission in mission_order: + if mission is None: + missions.append(None) + elif mission.type == "all_in": missions.append("All-In") elif get_option_value(world, player, "relegate_no_build") and mission.relegate: missions.append("no_build") else: missions.append(mission.type) - # Place Protoss Missions if we are not using ShuffleProtoss - if get_option_value(world, player, "shuffle_protoss") == 0: + # Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled + if get_option_value(world, player, "shuffle_protoss") == 0 and mission_order_type == 1: missions[22] = "A Sinister Turn" medium_pool.remove("A Sinister Turn") missions[23] = "Echoes of the Future" @@ -153,6 +130,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData # Search through missions to find slots needed to fill for i in range(len(missions)): + if missions[i] is None: + continue if missions[i] == "no_build": no_build_slots.append(i) elif missions[i] == "easy": @@ -163,28 +142,28 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData hard_slots.append(i) # Add no_build missions to the pool and fill in no_build slots - missions_to_add = no_build_pool + missions_to_add = mission_pools['no_build'] for slot in no_build_slots: filler = random.randint(0, len(missions_to_add)-1) missions[slot] = missions_to_add.pop(filler) # Add easy missions into pool and fill in easy slots - missions_to_add = missions_to_add + easy_pool + missions_to_add = missions_to_add + mission_pools['easy'] for slot in easy_slots: filler = random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) # Add medium missions into pool and fill in medium slots - missions_to_add = missions_to_add + medium_pool + missions_to_add = missions_to_add + mission_pools['medium'] for slot in medium_slots: filler = random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) # Add hard missions into pool and fill in hard slots - missions_to_add = missions_to_add + hard_pool + missions_to_add = missions_to_add + mission_pools['hard'] for slot in hard_slots: filler = random.randint(0, len(missions_to_add) - 1) @@ -195,7 +174,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData mission_req_table = {} for i in range(len(missions)): connections = [] - for connection in vanilla_shuffle_order[i].connect_to: + for connection in mission_order[i].connect_to: if connection == -1: connect(world, player, names, "Menu", missions[i]) else: @@ -203,14 +182,14 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData (lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and state._sc2wol_cleared_missions(world, player, missions_req))) - (missions[connection], vanilla_shuffle_order[i].number)) + (missions[connection], mission_order[i].number)) connections.append(connection + 1) mission_req_table.update({missions[i]: MissionInfo( vanilla_mission_req_table[missions[i]].id, vanilla_mission_req_table[missions[i]].extra_locations, - connections, vanilla_shuffle_order[i].category, number=vanilla_shuffle_order[i].number, - completion_critical=vanilla_shuffle_order[i].completion_critical, - or_requirements=vanilla_shuffle_order[i].or_requirements)}) + connections, mission_order[i].category, number=mission_order[i].number, + completion_critical=mission_order[i].completion_critical, + or_requirements=mission_order[i].or_requirements)}) return mission_req_table diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 6d056df808..4e9cc0c55c 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -9,6 +9,8 @@ from .Locations import get_locations from .Regions import create_regions from .Options import sc2wol_options, get_option_value from .LogicMixin import SC2WoLLogic +from .PoolFilter import filter_missions, filter_items +from .MissionTables import starting_mission_locations, MissionInfo class Starcraft2WoLWebWorld(WebWorld): @@ -64,9 +66,9 @@ class SC2WoLWorld(World): def generate_basic(self): excluded_items = get_excluded_items(self, self.world, self.player) - assign_starter_items(self.world, self.player, excluded_items, self.locked_locations) + starter_items = assign_starter_items(self.world, self.player, excluded_items, self.locked_locations) - pool = get_item_pool(self.world, self.player, excluded_items) + pool = get_item_pool(self.world, self.player, self.mission_req_table, starter_items, excluded_items, self.location_cache) fill_item_pool_with_dummy_items(self, self.world, self.player, self.locked_locations, self.location_cache, pool) @@ -123,7 +125,7 @@ def get_excluded_items(self: SC2WoLWorld, world: MultiWorld, player: int) -> Set return excluded_items -def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): +def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]) -> List[Item]: non_local_items = world.non_local_items[player].value local_basic_unit = tuple(item for item in basic_unit if item not in non_local_items) @@ -138,12 +140,11 @@ def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str else: first_location = first_location + ": Victory" - assign_starter_item(world, player, excluded_items, locked_locations, first_location, - local_basic_unit) + return [assign_starter_item(world, player, excluded_items, locked_locations, first_location, local_basic_unit)] def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str], - location: str, item_list: Tuple[str, ...]): + location: str, item_list: Tuple[str, ...]) -> Item: item_name = world.random.choice(item_list) @@ -155,8 +156,11 @@ def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str] locked_locations.append(location) + return item -def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]: + +def get_item_pool(world: MultiWorld, player: int, mission_req_table: dict[str, MissionInfo], + starter_items: List[str], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]: pool: List[Item] = [] for name, data in item_table.items(): @@ -165,7 +169,12 @@ def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> L item = create_item_with_correct_settings(world, player, name) pool.append(item) - return pool + existing_items = starter_items + [item.name for item in world.precollected_items[player]] + # For the future: goal items like Artifact Shards go here + locked_items = [] + + filtered_pool = filter_items(world, player, mission_req_table, location_cache, pool, existing_items, locked_items) + return filtered_pool def fill_item_pool_with_dummy_items(self: SC2WoLWorld, world: MultiWorld, player: int, locked_locations: List[str],