Archipelago/worlds/sc2wol/__init__.py

325 lines
13 KiB
Python

import typing
from typing import List, Set, Tuple, Dict
from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
from worlds.AutoWorld import WebWorld, World
from .Items import StarcraftWoLItem, filler_items, item_name_groups, get_item_table, get_full_item_list, \
get_basic_units, ItemData, upgrade_included_names, progressive_if_nco
from .Locations import get_locations, LocationType
from .Regions import create_regions
from .Options import sc2wol_options, get_option_value, LocationInclusion
from .LogicMixin import SC2WoLLogic
from .PoolFilter import filter_missions, filter_items, get_item_upgrades
from .MissionTables import starting_mission_locations, MissionInfo
class Starcraft2WoLWebWorld(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"]
)
tutorials = [setup]
class SC2WoLWorld(World):
"""
StarCraft II: Wings of Liberty is a science fiction real-time strategy video game developed and published by Blizzard Entertainment.
Command Raynor's Raiders in collecting pieces of the Keystone in order to stop the zerg threat posed by the Queen of Blades.
"""
game = "Starcraft 2 Wings of Liberty"
web = Starcraft2WoLWebWorld()
data_version = 5
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, None)}
option_definitions = sc2wol_options
item_name_groups = item_name_groups
locked_locations: typing.List[str]
location_cache: typing.List[Location]
mission_req_table = {}
final_mission_id: int
victory_item: str
required_client_version = 0, 4, 3
def __init__(self, multiworld: MultiWorld, player: int):
super(SC2WoLWorld, self).__init__(multiworld, player)
self.location_cache = []
self.locked_locations = []
def create_item(self, name: str) -> Item:
data = get_full_item_list()[name]
return StarcraftWoLItem(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.multiworld, self.player, get_locations(self.multiworld, self.player), self.location_cache
)
def create_items(self):
setup_events(self.player, self.locked_locations, self.location_cache)
excluded_items = get_excluded_items(self.multiworld, self.player)
starter_items = assign_starter_items(self.multiworld, self.player, excluded_items, self.locked_locations)
filter_locations(self.multiworld, self.player, self.locked_locations, self.location_cache)
pool = get_item_pool(self.multiworld, self.player, self.mission_req_table, starter_items, excluded_items, self.location_cache)
fill_item_pool_with_dummy_items(self, self.multiworld, self.player, 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.multiworld.random.choice(filler_items)
def fill_slot_data(self):
slot_data = {}
for option_name in sc2wol_options:
option = getattr(self.multiworld, option_name)[self.player]
if type(option.value) in {str, int}:
slot_data[option_name] = int(option.value)
slot_req_table = {}
for mission in self.mission_req_table:
slot_req_table[mission] = self.mission_req_table[mission]._asdict()
slot_data["mission_req"] = slot_req_table
slot_data["final_mission"] = self.final_mission_id
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(multiworld: MultiWorld, player: int) -> Set[str]:
excluded_items: Set[str] = set()
for item in multiworld.precollected_items[player]:
excluded_items.add(item.name)
excluded_items_option = getattr(multiworld, 'excluded_items', [])
excluded_items.update(excluded_items_option[player].value)
return excluded_items
def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]) -> List[Item]:
non_local_items = multiworld.non_local_items[player].value
if get_option_value(multiworld, player, "early_unit"):
local_basic_unit = sorted(item for item in get_basic_units(multiworld, player) if item not in non_local_items and item not in excluded_items)
if not local_basic_unit:
raise Exception("At least one basic unit must be local")
# The first world should also be the starting world
first_mission = list(multiworld.worlds[player].mission_req_table)[0]
if first_mission in starting_mission_locations:
first_location = starting_mission_locations[first_mission]
elif first_mission == "In Utter Darkness":
first_location = first_mission + ": Defeat"
else:
first_location = first_mission + ": Victory"
return [assign_starter_item(multiworld, player, excluded_items, locked_locations, first_location, local_basic_unit)]
else:
return []
def assign_starter_item(multiworld: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
location: str, item_list: Tuple[str, ...]) -> Item:
item_name = multiworld.random.choice(item_list)
excluded_items.add(item_name)
item = create_item_with_correct_settings(player, item_name)
multiworld.get_location(location, player).place_locked_item(item)
locked_locations.append(location)
return item
def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: 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(multiworld, player, 'locked_items')
# Adjust generic upgrade availability based on options
include_upgrades = get_option_value(multiworld, player, 'generic_upgrade_missions') == 0
upgrade_items = get_option_value(multiworld, player, 'generic_upgrade_items')
# Include items from outside Wings of Liberty
item_sets = {'wol'}
if get_option_value(multiworld, player, 'nco_items'):
item_sets.add('nco')
if get_option_value(multiworld, player, 'bw_items'):
item_sets.add('bw')
if get_option_value(multiworld, player, '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
else:
return data.quantity
for name, data in get_item_table(multiworld, player).items():
for i in range(allowed_quantity(name, data)):
item = create_item_with_correct_settings(player, name)
if name in yaml_locked_items:
locked_items.append(item)
else:
pool.append(item)
existing_items = starter_items + [item for item in multiworld.precollected_items[player]]
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)
filtered_pool = filter_items(multiworld, player, mission_req_table, location_cache, pool, existing_items, locked_items)
return filtered_pool
def fill_item_pool_with_dummy_items(self: SC2WoLWorld, multiworld: MultiWorld, player: int, 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(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: [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 filter_locations(multiworld: MultiWorld, player, 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(multiworld, player)
mission_progress_locations = get_option_value(multiworld, player, "mission_progress_locations")
bonus_locations = get_option_value(multiworld, player, "bonus_locations")
challenge_locations = get_option_value(multiworld, player, "challenge_locations")
optional_boss_locations = get_option_value(multiworld, player, "optional_boss_locations")
location_data = get_locations(multiworld, player)
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 = [sc2_location for sc2_location in location_data if sc2_location.name == location.name][0]
location_type = sc2_location.type
if location_type == LocationType.MISSION_PROGRESS \
and mission_progress_locations != LocationInclusion.option_enabled:
item_name = get_exclusion_item(multiworld, mission_progress_locations)
place_exclusion_item(item_name, location, locked_locations, player)
if location_type == LocationType.BONUS \
and bonus_locations != LocationInclusion.option_enabled:
item_name = get_exclusion_item(multiworld, bonus_locations)
place_exclusion_item(item_name, location, locked_locations, player)
if location_type == LocationType.CHALLENGE \
and challenge_locations != LocationInclusion.option_enabled:
item_name = get_exclusion_item(multiworld, challenge_locations)
place_exclusion_item(item_name, location, locked_locations, player)
if location_type == LocationType.OPTIONAL_BOSS \
and optional_boss_locations != LocationInclusion.option_enabled:
item_name = get_exclusion_item(multiworld, optional_boss_locations)
place_exclusion_item(item_name, location, locked_locations, player)
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 get_exclusion_item(multiworld: MultiWorld, option) -> str:
"""
Gets the exclusion item according to settings (trash/nothing)
:param multiworld:
:param option:
:return: Item used for location exclusion
"""
if option == LocationInclusion.option_nothing:
return "Nothing"
elif option == LocationInclusion.option_trash:
index = multiworld.random.randint(0, len(filler_items) - 1)
return filler_items[index]
raise Exception(f"Unsupported option type: {option}")
def get_plando_locations(multiworld: MultiWorld, player) -> List[str]:
"""
:param multiworld:
:param player:
:return: A list of locations affected by a plando in a world
"""
plando_locations = []
for plando_setting in multiworld.plando_items[player]:
plando_locations += plando_setting.get("locations", [])
plando_setting_location = plando_setting.get("location", None)
if plando_setting_location is not None:
plando_locations.append(plando_setting_location)
return plando_locations