482 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			482 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								import typing
							 | 
						||
| 
								 | 
							
								from dataclasses import fields
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from typing import List, Set, Iterable, Sequence, Dict, Callable, Union
							 | 
						||
| 
								 | 
							
								from math import floor, ceil
							 | 
						||
| 
								 | 
							
								from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
							 | 
						||
| 
								 | 
							
								from worlds.AutoWorld import WebWorld, World
							 | 
						||
| 
								 | 
							
								from . import ItemNames
							 | 
						||
| 
								 | 
							
								from .Items import StarcraftItem, filler_items, get_item_table, get_full_item_list, \
							 | 
						||
| 
								 | 
							
								    get_basic_units, ItemData, upgrade_included_names, progressive_if_nco, kerrigan_actives, kerrigan_passives, \
							 | 
						||
| 
								 | 
							
								    kerrigan_only_passives, progressive_if_ext, not_balanced_starting_units, spear_of_adun_calldowns, \
							 | 
						||
| 
								 | 
							
								    spear_of_adun_castable_passives, nova_equipment
							 | 
						||
| 
								 | 
							
								from .ItemGroups import item_name_groups
							 | 
						||
| 
								 | 
							
								from .Locations import get_locations, LocationType, get_location_types, get_plando_locations
							 | 
						||
| 
								 | 
							
								from .Regions import create_regions
							 | 
						||
| 
								 | 
							
								from .Options import get_option_value, LocationInclusion, KerriganLevelItemDistribution, \
							 | 
						||
| 
								 | 
							
								    KerriganPresence, KerriganPrimalStatus, RequiredTactics, kerrigan_unit_available, StarterUnit, SpearOfAdunPresence, \
							 | 
						||
| 
								 | 
							
								    get_enabled_campaigns, SpearOfAdunAutonomouslyCastAbilityPresence, Starcraft2Options
							 | 
						||
| 
								 | 
							
								from .PoolFilter import filter_items, get_item_upgrades, UPGRADABLE_ITEMS, missions_in_mission_table, get_used_races
							 | 
						||
| 
								 | 
							
								from .MissionTables import MissionInfo, SC2Campaign, lookup_name_to_mission, SC2Mission, \
							 | 
						||
| 
								 | 
							
								    SC2Race
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class Starcraft2WebWorld(WebWorld):
							 | 
						||
| 
								 | 
							
								    setup = Tutorial(
							 | 
						||
| 
								 | 
							
								        "Multiworld Setup Guide",
							 | 
						||
| 
								 | 
							
								        "A guide to setting up the Starcraft 2 randomizer connected to an Archipelago Multiworld",
							 | 
						||
| 
								 | 
							
								        "English",
							 | 
						||
| 
								 | 
							
								        "setup_en.md",
							 | 
						||
| 
								 | 
							
								        "setup/en",
							 | 
						||
| 
								 | 
							
								        ["TheCondor", "Phaneros"]
							 | 
						||
| 
								 | 
							
								    )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    tutorials = [setup]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class SC2World(World):
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    StarCraft II is a science fiction real-time strategy video game developed and published by Blizzard Entertainment.
							 | 
						||
| 
								 | 
							
								    Play as one of three factions across four campaigns in a battle for supremacy of the Koprulu Sector.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    game = "Starcraft 2"
							 | 
						||
| 
								 | 
							
								    web = Starcraft2WebWorld()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    item_name_to_id = {name: data.code for name, data in get_full_item_list().items()}
							 | 
						||
| 
								 | 
							
								    location_name_to_id = {location.name: location.code for location in get_locations(None)}
							 | 
						||
| 
								 | 
							
								    options_dataclass = Starcraft2Options
							 | 
						||
| 
								 | 
							
								    options: Starcraft2Options
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    item_name_groups = item_name_groups
							 | 
						||
| 
								 | 
							
								    locked_locations: typing.List[str]
							 | 
						||
| 
								 | 
							
								    location_cache: typing.List[Location]
							 | 
						||
| 
								 | 
							
								    mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = {}
							 | 
						||
| 
								 | 
							
								    final_mission_id: int
							 | 
						||
| 
								 | 
							
								    victory_item: str
							 | 
						||
| 
								 | 
							
								    required_client_version = 0, 4, 5
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def __init__(self, multiworld: MultiWorld, player: int):
							 | 
						||
| 
								 | 
							
								        super(SC2World, self).__init__(multiworld, player)
							 | 
						||
| 
								 | 
							
								        self.location_cache = []
							 | 
						||
| 
								 | 
							
								        self.locked_locations = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def create_item(self, name: str) -> Item:
							 | 
						||
| 
								 | 
							
								        data = get_full_item_list()[name]
							 | 
						||
| 
								 | 
							
								        return StarcraftItem(name, data.classification, data.code, self.player)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def create_regions(self):
							 | 
						||
| 
								 | 
							
								        self.mission_req_table, self.final_mission_id, self.victory_item = create_regions(
							 | 
						||
| 
								 | 
							
								            self, get_locations(self), self.location_cache
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def create_items(self):
							 | 
						||
| 
								 | 
							
								        setup_events(self.player, self.locked_locations, self.location_cache)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        excluded_items = get_excluded_items(self)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        starter_items = assign_starter_items(self, excluded_items, self.locked_locations, self.location_cache)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        fill_resource_locations(self, self.locked_locations, self.location_cache)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        pool = get_item_pool(self, self.mission_req_table, starter_items, excluded_items, self.location_cache)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        fill_item_pool_with_dummy_items(self, self.locked_locations, self.location_cache, pool)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.multiworld.itempool += pool
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def set_rules(self):
							 | 
						||
| 
								 | 
							
								        self.multiworld.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def get_filler_item_name(self) -> str:
							 | 
						||
| 
								 | 
							
								        return self.random.choice(filler_items)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def fill_slot_data(self):
							 | 
						||
| 
								 | 
							
								        slot_data = {}
							 | 
						||
| 
								 | 
							
								        for option_name in [field.name for field in fields(Starcraft2Options)]:
							 | 
						||
| 
								 | 
							
								            option = get_option_value(self, option_name)
							 | 
						||
| 
								 | 
							
								            if type(option) in {str, int}:
							 | 
						||
| 
								 | 
							
								                slot_data[option_name] = int(option)
							 | 
						||
| 
								 | 
							
								        slot_req_table = {}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Serialize data
							 | 
						||
| 
								 | 
							
								        for campaign in self.mission_req_table:
							 | 
						||
| 
								 | 
							
								            slot_req_table[campaign.id] = {}
							 | 
						||
| 
								 | 
							
								            for mission in self.mission_req_table[campaign]:
							 | 
						||
| 
								 | 
							
								                slot_req_table[campaign.id][mission] = self.mission_req_table[campaign][mission]._asdict()
							 | 
						||
| 
								 | 
							
								                # Replace mission objects with mission IDs
							 | 
						||
| 
								 | 
							
								                slot_req_table[campaign.id][mission]["mission"] = slot_req_table[campaign.id][mission]["mission"].id
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                for index in range(len(slot_req_table[campaign.id][mission]["required_world"])):
							 | 
						||
| 
								 | 
							
								                    # TODO this is a band-aid, sometimes the mission_req_table already contains dicts
							 | 
						||
| 
								 | 
							
								                    # as far as I can tell it's related to having multiple vanilla mission orders
							 | 
						||
| 
								 | 
							
								                    if not isinstance(slot_req_table[campaign.id][mission]["required_world"][index], dict):
							 | 
						||
| 
								 | 
							
								                        slot_req_table[campaign.id][mission]["required_world"][index] = slot_req_table[campaign.id][mission]["required_world"][index]._asdict()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        enabled_campaigns = get_enabled_campaigns(self)
							 | 
						||
| 
								 | 
							
								        slot_data["plando_locations"] = get_plando_locations(self)
							 | 
						||
| 
								 | 
							
								        slot_data["nova_covert_ops_only"] = (enabled_campaigns == {SC2Campaign.NCO})
							 | 
						||
| 
								 | 
							
								        slot_data["mission_req"] = slot_req_table
							 | 
						||
| 
								 | 
							
								        slot_data["final_mission"] = self.final_mission_id
							 | 
						||
| 
								 | 
							
								        slot_data["version"] = 3
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if SC2Campaign.HOTS not in enabled_campaigns:
							 | 
						||
| 
								 | 
							
								            slot_data["kerrigan_presence"] = KerriganPresence.option_not_present
							 | 
						||
| 
								 | 
							
								        return slot_data
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def setup_events(player: int, locked_locations: typing.List[str], location_cache: typing.List[Location]):
							 | 
						||
| 
								 | 
							
								    for location in location_cache:
							 | 
						||
| 
								 | 
							
								        if location.address is None:
							 | 
						||
| 
								 | 
							
								            item = Item(location.name, ItemClassification.progression, None, player)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            locked_locations.append(location.name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            location.place_locked_item(item)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_excluded_items(world: World) -> Set[str]:
							 | 
						||
| 
								 | 
							
								    excluded_items: Set[str] = set(get_option_value(world, 'excluded_items'))
							 | 
						||
| 
								 | 
							
								    for item in world.multiworld.precollected_items[world.player]:
							 | 
						||
| 
								 | 
							
								        excluded_items.add(item.name)
							 | 
						||
| 
								 | 
							
								    locked_items: Set[str] = set(get_option_value(world, 'locked_items'))
							 | 
						||
| 
								 | 
							
								    # Starter items are also excluded items
							 | 
						||
| 
								 | 
							
								    starter_items: Set[str] = set(get_option_value(world, 'start_inventory'))
							 | 
						||
| 
								 | 
							
								    item_table = get_full_item_list()
							 | 
						||
| 
								 | 
							
								    soa_presence = get_option_value(world, "spear_of_adun_presence")
							 | 
						||
| 
								 | 
							
								    soa_autocast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence")
							 | 
						||
| 
								 | 
							
								    enabled_campaigns = get_enabled_campaigns(world)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Ensure no item is both guaranteed and excluded
							 | 
						||
| 
								 | 
							
								    invalid_items = excluded_items.intersection(locked_items)
							 | 
						||
| 
								 | 
							
								    invalid_count = len(invalid_items)
							 | 
						||
| 
								 | 
							
								    # Don't count starter items that can appear multiple times
							 | 
						||
| 
								 | 
							
								    invalid_count -= len([item for item in starter_items.intersection(locked_items) if item_table[item].quantity != 1])
							 | 
						||
| 
								 | 
							
								    if invalid_count > 0:
							 | 
						||
| 
								 | 
							
								        raise Exception(f"{invalid_count} item{'s are' if invalid_count > 1 else ' is'} both locked and excluded from generation.  Please adjust your excluded items and locked items.")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def smart_exclude(item_choices: Set[str], choices_to_keep: int):
							 | 
						||
| 
								 | 
							
								        expected_choices = len(item_choices)
							 | 
						||
| 
								 | 
							
								        if expected_choices == 0:
							 | 
						||
| 
								 | 
							
								            return
							 | 
						||
| 
								 | 
							
								        item_choices = set(item_choices)
							 | 
						||
| 
								 | 
							
								        starter_choices = item_choices.intersection(starter_items)
							 | 
						||
| 
								 | 
							
								        excluded_choices = item_choices.intersection(excluded_items)
							 | 
						||
| 
								 | 
							
								        item_choices.difference_update(excluded_choices)
							 | 
						||
| 
								 | 
							
								        item_choices.difference_update(locked_items)
							 | 
						||
| 
								 | 
							
								        candidates = sorted(item_choices)
							 | 
						||
| 
								 | 
							
								        exclude_amount = min(expected_choices - choices_to_keep - len(excluded_choices) + len(starter_choices), len(candidates))
							 | 
						||
| 
								 | 
							
								        if exclude_amount > 0:
							 | 
						||
| 
								 | 
							
								            excluded_items.update(world.random.sample(candidates, exclude_amount))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Nova gear exclusion if NCO not in campaigns
							 | 
						||
| 
								 | 
							
								    if SC2Campaign.NCO not in enabled_campaigns:
							 | 
						||
| 
								 | 
							
								        excluded_items = excluded_items.union(nova_equipment)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    kerrigan_presence = get_option_value(world, "kerrigan_presence")
							 | 
						||
| 
								 | 
							
								    # Exclude Primal Form item if option is not set or Kerrigan is unavailable
							 | 
						||
| 
								 | 
							
								    if get_option_value(world, "kerrigan_primal_status") != KerriganPrimalStatus.option_item or \
							 | 
						||
| 
								 | 
							
								        (kerrigan_presence in {KerriganPresence.option_not_present, KerriganPresence.option_not_present_and_no_passives}):
							 | 
						||
| 
								 | 
							
								        excluded_items.add(ItemNames.KERRIGAN_PRIMAL_FORM)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # no Kerrigan & remove all passives => remove all abilities
							 | 
						||
| 
								 | 
							
								    if kerrigan_presence == KerriganPresence.option_not_present_and_no_passives:
							 | 
						||
| 
								 | 
							
								        for tier in range(7):
							 | 
						||
| 
								 | 
							
								            smart_exclude(kerrigan_actives[tier].union(kerrigan_passives[tier]), 0)
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        # no Kerrigan, but keep non-Kerrigan passives
							 | 
						||
| 
								 | 
							
								        if kerrigan_presence == KerriganPresence.option_not_present:
							 | 
						||
| 
								 | 
							
								            smart_exclude(kerrigan_only_passives, 0)
							 | 
						||
| 
								 | 
							
								            for tier in range(7):
							 | 
						||
| 
								 | 
							
								                smart_exclude(kerrigan_actives[tier], 0)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # SOA exclusion, other cases are handled by generic race logic
							 | 
						||
| 
								 | 
							
								    if (soa_presence == SpearOfAdunPresence.option_lotv_protoss and SC2Campaign.LOTV not in enabled_campaigns) \
							 | 
						||
| 
								 | 
							
								            or soa_presence == SpearOfAdunPresence.option_not_present:
							 | 
						||
| 
								 | 
							
								        excluded_items.update(spear_of_adun_calldowns)
							 | 
						||
| 
								 | 
							
								    if (soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_lotv_protoss \
							 | 
						||
| 
								 | 
							
								            and SC2Campaign.LOTV not in enabled_campaigns) \
							 | 
						||
| 
								 | 
							
								            or soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present:
							 | 
						||
| 
								 | 
							
								        excluded_items.update(spear_of_adun_castable_passives)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return excluded_items
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def assign_starter_items(world: World, excluded_items: Set[str], locked_locations: List[str], location_cache: typing.List[Location]) -> List[Item]:
							 | 
						||
| 
								 | 
							
								    starter_items: List[Item] = []
							 | 
						||
| 
								 | 
							
								    non_local_items = get_option_value(world, "non_local_items")
							 | 
						||
| 
								 | 
							
								    starter_unit = get_option_value(world, "starter_unit")
							 | 
						||
| 
								 | 
							
								    enabled_campaigns = get_enabled_campaigns(world)
							 | 
						||
| 
								 | 
							
								    first_mission = get_first_mission(world.mission_req_table)
							 | 
						||
| 
								 | 
							
								    # Ensuring that first mission is completable
							 | 
						||
| 
								 | 
							
								    if starter_unit == StarterUnit.option_off:
							 | 
						||
| 
								 | 
							
								        starter_mission_locations = [location.name for location in location_cache
							 | 
						||
| 
								 | 
							
								                                     if location.parent_region.name == first_mission
							 | 
						||
| 
								 | 
							
								                                     and location.access_rule == Location.access_rule]
							 | 
						||
| 
								 | 
							
								        if not starter_mission_locations:
							 | 
						||
| 
								 | 
							
								            # Force early unit if first mission is impossible without one
							 | 
						||
| 
								 | 
							
								            starter_unit = StarterUnit.option_any_starter_unit
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if starter_unit != StarterUnit.option_off:
							 | 
						||
| 
								 | 
							
								        first_race = lookup_name_to_mission[first_mission].race
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if first_race == SC2Race.ANY:
							 | 
						||
| 
								 | 
							
								            # If the first mission is a logic-less no-build
							 | 
						||
| 
								 | 
							
								            mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = world.mission_req_table
							 | 
						||
| 
								 | 
							
								            races = get_used_races(mission_req_table, world)
							 | 
						||
| 
								 | 
							
								            races.remove(SC2Race.ANY)
							 | 
						||
| 
								 | 
							
								            if lookup_name_to_mission[first_mission].race in races:
							 | 
						||
| 
								 | 
							
								                # The campaign's race is in (At least one mission that's not logic-less no-build exists)
							 | 
						||
| 
								 | 
							
								                first_race = lookup_name_to_mission[first_mission].campaign.race
							 | 
						||
| 
								 | 
							
								            elif len(races) > 0:
							 | 
						||
| 
								 | 
							
								                # The campaign only has logic-less no-build missions. Find any other valid race
							 | 
						||
| 
								 | 
							
								                first_race = world.random.choice(list(races))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if first_race != SC2Race.ANY:
							 | 
						||
| 
								 | 
							
								            # The race of the early unit has been chosen
							 | 
						||
| 
								 | 
							
								            basic_units = get_basic_units(world, first_race)
							 | 
						||
| 
								 | 
							
								            if starter_unit == StarterUnit.option_balanced:
							 | 
						||
| 
								 | 
							
								                basic_units = basic_units.difference(not_balanced_starting_units)
							 | 
						||
| 
								 | 
							
								            if first_mission == SC2Mission.DARK_WHISPERS.mission_name:
							 | 
						||
| 
								 | 
							
								                # Special case - you don't have a logicless location but need an AA
							 | 
						||
| 
								 | 
							
								                basic_units = basic_units.difference(
							 | 
						||
| 
								 | 
							
								                    {ItemNames.ZEALOT, ItemNames.CENTURION, ItemNames.SENTINEL, ItemNames.BLOOD_HUNTER,
							 | 
						||
| 
								 | 
							
								                     ItemNames.AVENGER, ItemNames.IMMORTAL, ItemNames.ANNIHILATOR, ItemNames.VANGUARD})
							 | 
						||
| 
								 | 
							
								            if first_mission == SC2Mission.SUDDEN_STRIKE.mission_name:
							 | 
						||
| 
								 | 
							
								                # Special case - cliffjumpers
							 | 
						||
| 
								 | 
							
								                basic_units = {ItemNames.REAPER, ItemNames.GOLIATH, ItemNames.SIEGE_TANK, ItemNames.VIKING, ItemNames.BANSHEE}
							 | 
						||
| 
								 | 
							
								            local_basic_unit = sorted(item for item in basic_units if item not in non_local_items and item not in excluded_items)
							 | 
						||
| 
								 | 
							
								            if not local_basic_unit:
							 | 
						||
| 
								 | 
							
								                # Drop non_local_items constraint
							 | 
						||
| 
								 | 
							
								                local_basic_unit = sorted(item for item in basic_units if item not in excluded_items)
							 | 
						||
| 
								 | 
							
								                if not local_basic_unit:
							 | 
						||
| 
								 | 
							
								                    raise Exception("Early Unit: At least one basic unit must be included")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            unit: Item = add_starter_item(world, excluded_items, local_basic_unit)
							 | 
						||
| 
								 | 
							
								            starter_items.append(unit)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            # NCO-only specific rules
							 | 
						||
| 
								 | 
							
								            if first_mission == SC2Mission.SUDDEN_STRIKE.mission_name:
							 | 
						||
| 
								 | 
							
								                support_item: Union[str, None] = None
							 | 
						||
| 
								 | 
							
								                if unit.name == ItemNames.REAPER:
							 | 
						||
| 
								 | 
							
								                    support_item = ItemNames.REAPER_SPIDER_MINES
							 | 
						||
| 
								 | 
							
								                elif unit.name == ItemNames.GOLIATH:
							 | 
						||
| 
								 | 
							
								                    support_item = ItemNames.GOLIATH_JUMP_JETS
							 | 
						||
| 
								 | 
							
								                elif unit.name == ItemNames.SIEGE_TANK:
							 | 
						||
| 
								 | 
							
								                    support_item = ItemNames.SIEGE_TANK_JUMP_JETS
							 | 
						||
| 
								 | 
							
								                elif unit.name == ItemNames.VIKING:
							 | 
						||
| 
								 | 
							
								                    support_item = ItemNames.VIKING_SMART_SERVOS
							 | 
						||
| 
								 | 
							
								                if support_item is not None:
							 | 
						||
| 
								 | 
							
								                    starter_items.append(add_starter_item(world, excluded_items, [support_item]))
							 | 
						||
| 
								 | 
							
								                starter_items.append(add_starter_item(world, excluded_items, [ItemNames.NOVA_JUMP_SUIT_MODULE]))
							 | 
						||
| 
								 | 
							
								                starter_items.append(
							 | 
						||
| 
								 | 
							
								                    add_starter_item(world, excluded_items,
							 | 
						||
| 
								 | 
							
								                                     [
							 | 
						||
| 
								 | 
							
								                                         ItemNames.NOVA_HELLFIRE_SHOTGUN,
							 | 
						||
| 
								 | 
							
								                                         ItemNames.NOVA_PLASMA_RIFLE,
							 | 
						||
| 
								 | 
							
								                                         ItemNames.NOVA_PULSE_GRENADES
							 | 
						||
| 
								 | 
							
								                                     ]))
							 | 
						||
| 
								 | 
							
								            if enabled_campaigns == {SC2Campaign.NCO}:
							 | 
						||
| 
								 | 
							
								                starter_items.append(add_starter_item(world, excluded_items, [ItemNames.LIBERATOR_RAID_ARTILLERY]))
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    starter_abilities = get_option_value(world, 'start_primary_abilities')
							 | 
						||
| 
								 | 
							
								    assert isinstance(starter_abilities, int)
							 | 
						||
| 
								 | 
							
								    if starter_abilities:
							 | 
						||
| 
								 | 
							
								        ability_count = starter_abilities
							 | 
						||
| 
								 | 
							
								        ability_tiers = [0, 1, 3]
							 | 
						||
| 
								 | 
							
								        world.random.shuffle(ability_tiers)
							 | 
						||
| 
								 | 
							
								        if ability_count > 3:
							 | 
						||
| 
								 | 
							
								            ability_tiers.append(6)
							 | 
						||
| 
								 | 
							
								        for tier in ability_tiers:
							 | 
						||
| 
								 | 
							
								            abilities = kerrigan_actives[tier].union(kerrigan_passives[tier]).difference(excluded_items, non_local_items)
							 | 
						||
| 
								 | 
							
								            if not abilities:
							 | 
						||
| 
								 | 
							
								                abilities = kerrigan_actives[tier].union(kerrigan_passives[tier]).difference(excluded_items)
							 | 
						||
| 
								 | 
							
								            if abilities:
							 | 
						||
| 
								 | 
							
								                ability_count -= 1
							 | 
						||
| 
								 | 
							
								                starter_items.append(add_starter_item(world, excluded_items, list(abilities)))
							 | 
						||
| 
								 | 
							
								                if ability_count == 0:
							 | 
						||
| 
								 | 
							
								                    break
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return starter_items
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_first_mission(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]]) -> str:
							 | 
						||
| 
								 | 
							
								    # The first world should also be the starting world
							 | 
						||
| 
								 | 
							
								    campaigns = mission_req_table.keys()
							 | 
						||
| 
								 | 
							
								    lowest_id = min([campaign.id for campaign in campaigns])
							 | 
						||
| 
								 | 
							
								    first_campaign = [campaign for campaign in campaigns if campaign.id == lowest_id][0]
							 | 
						||
| 
								 | 
							
								    first_mission = list(mission_req_table[first_campaign])[0]
							 | 
						||
| 
								 | 
							
								    return first_mission
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def add_starter_item(world: World, excluded_items: Set[str], item_list: Sequence[str]) -> Item:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    item_name = world.random.choice(sorted(item_list))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    excluded_items.add(item_name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    item = create_item_with_correct_settings(world.player, item_name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    world.multiworld.push_precollected(item)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return item
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_item_pool(world: World, mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]],
							 | 
						||
| 
								 | 
							
								                  starter_items: List[Item], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
							 | 
						||
| 
								 | 
							
								    pool: List[Item] = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # For the future: goal items like Artifact Shards go here
							 | 
						||
| 
								 | 
							
								    locked_items = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # YAML items
							 | 
						||
| 
								 | 
							
								    yaml_locked_items = get_option_value(world, 'locked_items')
							 | 
						||
| 
								 | 
							
								    assert not isinstance(yaml_locked_items, int)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Adjust generic upgrade availability based on options
							 | 
						||
| 
								 | 
							
								    include_upgrades = get_option_value(world, 'generic_upgrade_missions') == 0
							 | 
						||
| 
								 | 
							
								    upgrade_items = get_option_value(world, 'generic_upgrade_items')
							 | 
						||
| 
								 | 
							
								    assert isinstance(upgrade_items, int)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Include items from outside main campaigns
							 | 
						||
| 
								 | 
							
								    item_sets = {'wol', 'hots', 'lotv'}
							 | 
						||
| 
								 | 
							
								    if get_option_value(world, 'nco_items') \
							 | 
						||
| 
								 | 
							
								            or SC2Campaign.NCO in get_enabled_campaigns(world):
							 | 
						||
| 
								 | 
							
								        item_sets.add('nco')
							 | 
						||
| 
								 | 
							
								    if get_option_value(world, 'bw_items'):
							 | 
						||
| 
								 | 
							
								        item_sets.add('bw')
							 | 
						||
| 
								 | 
							
								    if get_option_value(world, 'ext_items'):
							 | 
						||
| 
								 | 
							
								        item_sets.add('ext')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def allowed_quantity(name: str, data: ItemData) -> int:
							 | 
						||
| 
								 | 
							
								        if name in excluded_items \
							 | 
						||
| 
								 | 
							
								                or data.type == "Upgrade" and (not include_upgrades or name not in upgrade_included_names[upgrade_items]) \
							 | 
						||
| 
								 | 
							
								                or not data.origin.intersection(item_sets):
							 | 
						||
| 
								 | 
							
								            return 0
							 | 
						||
| 
								 | 
							
								        elif name in progressive_if_nco and 'nco' not in item_sets:
							 | 
						||
| 
								 | 
							
								            return 1
							 | 
						||
| 
								 | 
							
								        elif name in progressive_if_ext and 'ext' not in item_sets:
							 | 
						||
| 
								 | 
							
								            return 1
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return data.quantity
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for name, data in get_item_table().items():
							 | 
						||
| 
								 | 
							
								        for _ in range(allowed_quantity(name, data)):
							 | 
						||
| 
								 | 
							
								            item = create_item_with_correct_settings(world.player, name)
							 | 
						||
| 
								 | 
							
								            if name in yaml_locked_items:
							 | 
						||
| 
								 | 
							
								                locked_items.append(item)
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                pool.append(item)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    existing_items = starter_items + [item for item in world.multiworld.precollected_items[world.player] if item not in starter_items]
							 | 
						||
| 
								 | 
							
								    existing_names = [item.name for item in existing_items]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Check the parent item integrity, exclude items
							 | 
						||
| 
								 | 
							
								    pool[:] = [item for item in pool if pool_contains_parent(item, pool + locked_items + existing_items)]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Removing upgrades for excluded items
							 | 
						||
| 
								 | 
							
								    for item_name in excluded_items:
							 | 
						||
| 
								 | 
							
								        if item_name in existing_names:
							 | 
						||
| 
								 | 
							
								            continue
							 | 
						||
| 
								 | 
							
								        invalid_upgrades = get_item_upgrades(pool, item_name)
							 | 
						||
| 
								 | 
							
								        for invalid_upgrade in invalid_upgrades:
							 | 
						||
| 
								 | 
							
								            pool.remove(invalid_upgrade)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    fill_pool_with_kerrigan_levels(world, pool)
							 | 
						||
| 
								 | 
							
								    filtered_pool = filter_items(world, mission_req_table, location_cache, pool, existing_items, locked_items)
							 | 
						||
| 
								 | 
							
								    return filtered_pool
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def fill_item_pool_with_dummy_items(self: SC2World, locked_locations: List[str],
							 | 
						||
| 
								 | 
							
								                                    location_cache: List[Location], pool: List[Item]):
							 | 
						||
| 
								 | 
							
								    for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
							 | 
						||
| 
								 | 
							
								        item = create_item_with_correct_settings(self.player, self.get_filler_item_name())
							 | 
						||
| 
								 | 
							
								        pool.append(item)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def create_item_with_correct_settings(player: int, name: str) -> Item:
							 | 
						||
| 
								 | 
							
								    data = get_full_item_list()[name]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    item = Item(name, data.classification, data.code, player)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return item
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def pool_contains_parent(item: Item, pool: Iterable[Item]):
							 | 
						||
| 
								 | 
							
								    item_data = get_full_item_list().get(item.name)
							 | 
						||
| 
								 | 
							
								    if item_data.parent_item is None:
							 | 
						||
| 
								 | 
							
								        # The item has not associated parent, the item is valid
							 | 
						||
| 
								 | 
							
								        return True
							 | 
						||
| 
								 | 
							
								    parent_item = item_data.parent_item
							 | 
						||
| 
								 | 
							
								    # Check if the pool contains the parent item
							 | 
						||
| 
								 | 
							
								    return parent_item in [pool_item.name for pool_item in pool]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def fill_resource_locations(world: World, locked_locations: List[str], location_cache: List[Location]):
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    Filters the locations in the world using a trash or Nothing item
							 | 
						||
| 
								 | 
							
								    :param multiworld:
							 | 
						||
| 
								 | 
							
								    :param player:
							 | 
						||
| 
								 | 
							
								    :param locked_locations:
							 | 
						||
| 
								 | 
							
								    :param location_cache:
							 | 
						||
| 
								 | 
							
								    :return:
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    open_locations = [location for location in location_cache if location.item is None]
							 | 
						||
| 
								 | 
							
								    plando_locations = get_plando_locations(world)
							 | 
						||
| 
								 | 
							
								    resource_location_types = get_location_types(world, LocationInclusion.option_resources)
							 | 
						||
| 
								 | 
							
								    location_data = {sc2_location.name: sc2_location for sc2_location in get_locations(world)}
							 | 
						||
| 
								 | 
							
								    for location in open_locations:
							 | 
						||
| 
								 | 
							
								        # Go through the locations that aren't locked yet (early unit, etc)
							 | 
						||
| 
								 | 
							
								        if location.name not in plando_locations:
							 | 
						||
| 
								 | 
							
								            # The location is not plando'd
							 | 
						||
| 
								 | 
							
								            sc2_location = location_data[location.name]
							 | 
						||
| 
								 | 
							
								            if sc2_location.type in resource_location_types:
							 | 
						||
| 
								 | 
							
								                item_name = world.random.choice(filler_items)
							 | 
						||
| 
								 | 
							
								                item = create_item_with_correct_settings(world.player, item_name)
							 | 
						||
| 
								 | 
							
								                location.place_locked_item(item)
							 | 
						||
| 
								 | 
							
								                locked_locations.append(location.name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def place_exclusion_item(item_name, location, locked_locations, player):
							 | 
						||
| 
								 | 
							
								    item = create_item_with_correct_settings(player, item_name)
							 | 
						||
| 
								 | 
							
								    location.place_locked_item(item)
							 | 
						||
| 
								 | 
							
								    locked_locations.append(location.name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def fill_pool_with_kerrigan_levels(world: World, item_pool: List[Item]):
							 | 
						||
| 
								 | 
							
								    total_levels = get_option_value(world, "kerrigan_level_item_sum")
							 | 
						||
| 
								 | 
							
								    if get_option_value(world, "kerrigan_presence") not in kerrigan_unit_available \
							 | 
						||
| 
								 | 
							
								            or total_levels == 0 \
							 | 
						||
| 
								 | 
							
								            or SC2Campaign.HOTS not in get_enabled_campaigns(world):
							 | 
						||
| 
								 | 
							
								        return
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    def add_kerrigan_level_items(level_amount: int, item_amount: int):
							 | 
						||
| 
								 | 
							
								        name = f"{level_amount} Kerrigan Level"
							 | 
						||
| 
								 | 
							
								        if level_amount > 1:
							 | 
						||
| 
								 | 
							
								            name += "s"
							 | 
						||
| 
								 | 
							
								        for _ in range(item_amount):
							 | 
						||
| 
								 | 
							
								            item_pool.append(create_item_with_correct_settings(world.player, name))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    sizes = [70, 35, 14, 10, 7, 5, 2, 1]
							 | 
						||
| 
								 | 
							
								    option = get_option_value(world, "kerrigan_level_item_distribution")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    assert isinstance(option, int)
							 | 
						||
| 
								 | 
							
								    assert isinstance(total_levels, int)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if option in (KerriganLevelItemDistribution.option_vanilla, KerriganLevelItemDistribution.option_smooth):
							 | 
						||
| 
								 | 
							
								        distribution = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
							 | 
						||
| 
								 | 
							
								        if option == KerriganLevelItemDistribution.option_vanilla:
							 | 
						||
| 
								 | 
							
								            distribution = [32, 0, 0, 1, 3, 0, 0, 0, 1, 1]
							 | 
						||
| 
								 | 
							
								        else: # Smooth
							 | 
						||
| 
								 | 
							
								            distribution = [0, 0, 0, 1, 1, 2, 2, 2, 1, 1]
							 | 
						||
| 
								 | 
							
								        for tier in range(len(distribution)):
							 | 
						||
| 
								 | 
							
								            add_kerrigan_level_items(tier + 1, distribution[tier])
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        size = sizes[option - 2]
							 | 
						||
| 
								 | 
							
								        round_func: Callable[[float], int] = round
							 | 
						||
| 
								 | 
							
								        if total_levels > 70:
							 | 
						||
| 
								 | 
							
								            round_func = floor
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            round_func = ceil
							 | 
						||
| 
								 | 
							
								        add_kerrigan_level_items(size, round_func(float(total_levels) / size))
							 |