diff --git a/Launcher.py b/Launcher.py index 9ebba4e6..7c53a120 100644 --- a/Launcher.py +++ b/Launcher.py @@ -152,6 +152,8 @@ components: Iterable[Component] = ( Component('FF1 Client', 'FF1Client'), # ChecksFinder Component('ChecksFinder Client', 'ChecksFinderClient'), + # Starcraft 2 + Component('Starcraft 2 Client', 'StarcraftClient'), # Functions Component('Open host.yaml', func=open_host_yaml), Component('Open Patch', func=open_patch), diff --git a/README.md b/README.md index e073c383..6b6dac41 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Currently, the following games are supported: * Hollow Knight * The Witness * Sonic Adventure 2: Battle +* Starcraft 2: Wings of Liberty For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/StarcraftClient.py b/StarcraftClient.py new file mode 100644 index 00000000..db4402d9 --- /dev/null +++ b/StarcraftClient.py @@ -0,0 +1,520 @@ +from __future__ import annotations + +import sys +import multiprocessing +import logging +import asyncio +import nest_asyncio + +import sc2 + +from sc2.main import run_game +from sc2.data import Race +from sc2.bot_ai import BotAI +from sc2.player import Bot +from worlds.sc2wol.Items import lookup_id_to_name, item_table +from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET + +from Utils import init_logging + +if __name__ == "__main__": + init_logging("SC2Client", exception_logger="Client") + +logger = logging.getLogger("Client") +sc2_logger = logging.getLogger("Starcraft2") + +import colorama + +from NetUtils import * +from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, get_base_parser + +nest_asyncio.apply() + +class StarcraftClientProcessor(ClientCommandProcessor): + ctx: Context + + def _cmd_play(self, mission_id: str = "") -> bool: + """Start a Starcraft 2 mission""" + + options = mission_id.split() + num_options = len(options) + + if num_options > 0: + mission_number = int(options[0]) + + if is_mission_available(mission_number, self.ctx.checked_locations, mission_req_table): + asyncio.create_task(starcraft_launch(self.ctx, mission_number), name="Starcraft Launch") + else: + sc2_logger.info( + "This mission is not currently unlocked. Use /unfinished or /available to see what is available.") + + else: + sc2_logger.info("Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.") + + return True + + def _cmd_available(self) -> bool: + """Get what missions are currently available to play""" + + request_available_missions(self.ctx.checked_locations, mission_req_table) + return True + + def _cmd_unfinished(self) -> bool: + """Get what missions are currently available to play and have not had all locations checked""" + + request_unfinished_missions(self.ctx.checked_locations, mission_req_table) + return True + + +class Context(CommonContext): + command_processor = StarcraftClientProcessor + game = "Starcraft 2 Wings of Liberty" + items_handling = 0b111 + difficulty = -1 + all_in_choice = 0 + items_rec_to_announce = [] + rec_announce_pos = 0 + items_sent_to_announce = [] + sent_announce_pos = 0 + announcements = [] + announcement_pos = 0 + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(Context, self).server_auth(password_requested) + if not self.auth: + logger.info('Enter slot name:') + self.auth = await self.console_input() + + await self.send_connect() + + def on_package(self, cmd: str, args: dict): + if cmd in {"Connected"}: + self.difficulty = args["slot_data"]["game_difficulty"] + self.all_in_choice = args["slot_data"]["all_in_map"] + if cmd in {"PrintJSON"}: + noted = False + if "receiving" in args: + if args["receiving"] == self.slot: + self.announcements.append(args["data"]) + noted = True + if not noted and "item" in args: + if args["item"].player == self.slot: + self.announcements.append(args["data"]) + + def run_gui(self): + from kvui import GameManager + + class SC2Manager(GameManager): + logging_pairs = [ + ("Client", "Archipelago"), + ("Starcraft2", "Starcraft2"), + ] + base_title = "Archipelago Starcraft 2 Client" + + self.ui = SC2Manager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +async def main(): + multiprocessing.freeze_support() + parser = get_base_parser() + parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) + args = parser.parse_args() + + ctx = Context(args.connect, args.password) + if ctx.server_task is None: + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + + input_task = None + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + if sys.stdin: + input_task = asyncio.create_task(console_loop(ctx), name="Input") + + await ctx.exit_event.wait() + + ctx.server_address = None + ctx.snes_reconnect_address = None + await ctx.shutdown() + + if ui_task: + await ui_task + + if input_task: + input_task.cancel() + +maps_table = ["ap_traynor01", "ap_traynor02", "ap_traynor03", "ap_thanson01", "ap_thanson02", "ap_thanson03a", "ap_thanson03b", "ap_ttychus01", + "ap_ttychus02", "ap_ttychus03", "ap_ttychus04", "ap_ttychus05", "ap_ttosh01", "ap_ttosh02", "ap_ttosh03a", "ap_ttosh03b", + "ap_thorner01", "ap_thorner02", "ap_thorner03", "ap_thorner04", "ap_thorner05s", "ap_tzeratul01", "ap_tzeratul02", + "ap_tzeratul03", "ap_tzeratul04", "ap_tvalerian01", "ap_tvalerian02a", "ap_tvalerian02b", "ap_tvalerian03"] + + +def calculate_items(items): + unit_unlocks = 0 + armory1_unlocks = 0 + armory2_unlocks = 0 + upgrade_unlocks = 0 + building_unlocks = 0 + merc_unlocks = 0 + lab_unlocks = 0 + protoss_unlock = 0 + minerals = 0 + vespene = 0 + + for item in items: + data = lookup_id_to_name[item.item] + + if item_table[data].type == "Unit": + unit_unlocks += (1 << item_table[data].number) + elif item_table[data].type == "Upgrade": + upgrade_unlocks += (1 << item_table[data].number) + elif item_table[data].type == "Armory 1": + armory1_unlocks += (1 << item_table[data].number) + elif item_table[data].type == "Armory 2": + armory2_unlocks += (1 << item_table[data].number) + elif item_table[data].type == "Building": + building_unlocks += (1 << item_table[data].number) + elif item_table[data].type == "Mercenary": + merc_unlocks += (1 << item_table[data].number) + elif item_table[data].type == "Laboratory": + lab_unlocks += (1 << item_table[data].number) + elif item_table[data].type == "Protoss": + protoss_unlock += (1 << item_table[data].number) + elif item_table[data].type == "Minerals": + minerals += item_table[data].number + elif item_table[data].type == "Vespene": + vespene += item_table[data].number + + return [unit_unlocks, upgrade_unlocks, armory1_unlocks, armory2_unlocks, building_unlocks, merc_unlocks, + lab_unlocks, protoss_unlock, minerals, vespene] + + +def calc_difficulty(difficulty): + if difficulty == 0: + return 'C' + elif difficulty == 1: + return 'N' + elif difficulty == 2: + return 'H' + elif difficulty == 3: + return 'B' + + return 'X' + + +async def starcraft_launch(ctx: Context, mission_id): + ctx.rec_announce_pos = len(ctx.items_rec_to_announce) + ctx.sent_announce_pos = len(ctx.items_sent_to_announce) + ctx.announcements_pos = len(ctx.announcements) + + run_game(sc2.maps.get(maps_table[mission_id-1]), [ + Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), name="Archipelago")], realtime=True) + + +class ArchipelagoBot(sc2.bot_ai.BotAI): + game_running = False + mission_completed = False + first_bonus = False + second_bonus = False + third_bonus = False + fourth_bonus = False + fifth_bonus = False + sixth_bonus = False + seventh_bonus = False + eight_bonus = False + ctx: Context = None + mission_id = 0 + + can_read_game = False + + last_received_update = 0 + + def __init__(self, ctx: Context, mission_id): + self.ctx = ctx + self.mission_id = mission_id + + super(ArchipelagoBot, self).__init__() + + async def on_step(self, iteration: int): + game_state = 0 + if iteration == 0: + start_items = calculate_items(self.ctx.items_received) + difficulty = calc_difficulty(self.ctx.difficulty) + await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {}".format( + difficulty, start_items[0], start_items[1], start_items[2], start_items[3], start_items[4], start_items[5], + start_items[6], start_items[7], start_items[8], start_items[9], self.ctx.all_in_choice)) + self.last_received_update = len(self.ctx.items_received) + + else: + if self.ctx.announcement_pos < len(self.ctx.announcements): + index = 0 + message = "" + while index < len(self.ctx.announcements[self.ctx.announcement_pos]): + message += self.ctx.announcements[self.ctx.announcement_pos][index]["text"] + index += 1 + + index = 0 + start_rem_pos = -1 + # Remove unneeded [Color] tags + while index < len(message): + if message[index] == '[': + start_rem_pos = index + index += 1 + elif message[index] == ']' and start_rem_pos > -1: + temp_msg = "" + + if start_rem_pos > 0: + temp_msg = message[:start_rem_pos] + if index < len(message) - 1: + temp_msg += message[index+1:] + + message = temp_msg + index += start_rem_pos - index + start_rem_pos = -1 + else: + index += 1 + + await self.chat_send("SendMessage " + message) + self.ctx.announcement_pos += 1 + + # Archipelago reads the health + for unit in self.all_own_units(): + if unit.health_max == 38281: + game_state = int(38281 - unit.health) + can_read_game = True + + if iteration == 80 and not game_state & 1: + await self.chat_send("SendMessage Warning: Archipelago unable to connect or has lost connection to " + + "Starcraft 2 (This is likely a map issue)") + + if self.last_received_update < len(self.ctx.items_received): + current_items = calculate_items(self.ctx.items_received) + await self.chat_send("UpdateTech {} {} {} {} {} {} {} {}".format( + current_items[0], current_items[1], current_items[2], current_items[3], current_items[4], current_items[5], + current_items[6], current_items[7])) + self.last_received_update = len(self.ctx.items_received) + + if game_state & 1: + if not self.game_running: + print("Archipelago Connected") + self.game_running = True + + if can_read_game: + if game_state & (1 << 1) and not self.mission_completed: + if self.mission_id != 29: + print("Mission Completed") + await self.ctx.send_msgs([ + {"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id]}]) + self.mission_completed = True + else: + print("Game Complete") + await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}]) + self.mission_completed = True + + if game_state & (1 << 2) and not self.first_bonus: + print("1st Bonus Collected") + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 1]}]) + self.first_bonus = True + + if not self.second_bonus and game_state & (1 << 3): + print("2nd Bonus Collected") + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 2]}]) + self.second_bonus = True + + if not self.third_bonus and game_state & (1 << 4): + print("3rd Bonus Collected") + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 3]}]) + self.third_bonus = True + + if not self.fourth_bonus and game_state & (1 << 5): + print("4th Bonus Collected") + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 4]}]) + self.fourth_bonus = True + + if not self.fifth_bonus and game_state & (1 << 6): + print("5th Bonus Collected") + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 5]}]) + self.fifth_bonus = True + + if not self.sixth_bonus and game_state & (1 << 7): + print("6th Bonus Collected") + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 6]}]) + self.sixth_bonus = True + + if not self.seventh_bonus and game_state & (1 << 8): + print("6th Bonus Collected") + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 7]}]) + self.seventh_bonus = True + + if not self.eight_bonus and game_state & (1 << 9): + print("6th Bonus Collected") + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 8]}]) + self.eight_bonus = True + + else: + await self.chat_send("LostConnection - Lost connection to game.") + + +class MissionInfo(typing.NamedTuple): + id: int + extra_locations: int + required_world: list[int] + number: int = 0 # number of worlds need beaten + + +mission_req_table = { + "Liberation Day": MissionInfo(1, 7, []), + "The Outlaws": MissionInfo(2, 2, [1]), + "Zero Hour": MissionInfo(3, 4, [2]), + "Evacuation": MissionInfo(4, 4, [3]), + "Outbreak": MissionInfo(5, 3, [4]), + "Safe Haven": MissionInfo(6, 1, [5], number=7), + "Haven's Fall": MissionInfo(7, 1, [5], number=7), + "Smash and Grab": MissionInfo(8, 5, [3]), + "The Dig": MissionInfo(9, 4, [8], number=8), + "The Moebius Factor": MissionInfo(10, 9, [9], number=11), + "Supernova": MissionInfo(11, 5, [10], number=14), + "Maw of the Void": MissionInfo(12, 6, [11]), + "Devil's Playground": MissionInfo(13, 3, [3], number=4), + "Welcome to the Jungle": MissionInfo(14, 4, [13]), + "Breakout": MissionInfo(15, 3, [14], number=8), + "Ghost of a Chance": MissionInfo(16, 6, [14], number=8), + "The Great Train Robbery": MissionInfo(17, 4, [3], number=6), + "Cutthroat": MissionInfo(18, 5, [17]), + "Engine of Destruction": MissionInfo(19, 6, [18]), + "Media Blitz": MissionInfo(20, 5, [19]), + "Piercing the Shroud": MissionInfo(21, 6, [20]), + "Whispers of Doom": MissionInfo(22, 4, [9]), + "A Sinister Turn": MissionInfo(23, 4, [22]), + "Echoes of the Future": MissionInfo(24, 3, [23]), + "In Utter Darkness": MissionInfo(25, 3, [24]), + "Gates of Hell": MissionInfo(26, 2, [12]), + "Belly of the Beast": MissionInfo(27, 4, [26]), + "Shatter the Sky": MissionInfo(28, 5, [26]), + "All-In": MissionInfo(29, -1, [27, 28]) +} + + +def calc_objectives_completed(mission, missions_info, locations_done): + objectives_complete = 0 + + if missions_info[mission].extra_locations > 0: + for i in range(missions_info[mission].extra_locations): + if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done: + objectives_complete += 1 + + return objectives_complete + + else: + return -1 + + +def request_unfinished_missions(locations_done, location_table): + message = "Unfinished Missions:" + + first_item = True + + unfinished_missions = calc_unfinished_missions(locations_done, location_table) + + for mission in unfinished_missions: + if first_item: + message += " {}[{}] ({}/{})".format(mission, location_table[mission].id, unfinished_missions[mission], + location_table[mission].extra_locations) + first_item = False + else: + message += ", {}[{}] ({}/{})".format(mission, location_table[mission].id, unfinished_missions[mission], + location_table[mission].extra_locations) + + sc2_logger.info(message) + + +def calc_unfinished_missions(locations_done, locations): + unfinished_missions = [] + locations_completed = [] + available_missions = calc_available_missions(locations_done, locations) + + for name in available_missions: + if not locations[name].extra_locations == -1: + objectives_completed = calc_objectives_completed(name, locations, locations_done) + + if objectives_completed < locations[name].extra_locations: + unfinished_missions.append(name) + locations_completed.append(objectives_completed) + + else: + unfinished_missions.append(name) + locations_completed.append(-1) + + return {unfinished_missions[i]: locations_completed[i] for i in range(len(unfinished_missions))} + + +def is_mission_available(mission_id_to_check, locations_done, locations): + unfinished_missions = calc_available_missions(locations_done, locations) + + for mission in unfinished_missions: + if locations[mission].id == mission_id_to_check: + return True + + return False + + +def request_available_missions(locations_done, location_table): + message = "Available Missions:" + + first_item = True + + missions = calc_available_missions(locations_done, location_table) + + for mission in missions: + if first_item: + message += " {}[{}]".format(mission, location_table[mission].id) + first_item = False + else: + message += ", {}[{}]".format(mission, location_table[mission].id) + + sc2_logger.info(message) + + +def calc_available_missions(locations_done, locations): + available_missions = [] + mission_complete = 0 + + # Get number of missions completed + for loc in locations_done: + if loc % 100 == 0: + mission_complete += 1 + + for name in locations: + if len(locations[name].required_world) >= 1: + reqs_complete = True + + for req_mission in locations[name].required_world: + if not(req_mission * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done: + reqs_complete = False + break + + if reqs_complete and mission_complete >= locations[name].number: + available_missions.append(name) + else: + available_missions.append(name) + + return available_missions + + +if __name__ == '__main__': + colorama.init() + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + loop.close() + colorama.deinit() diff --git a/inno_setup.iss b/inno_setup.iss index 3ecb93c1..7f5d11f0 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -67,6 +67,7 @@ Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDi Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing Name: "client/cf"; Description: "ChecksFinder"; Types: full playing +Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing [Dirs] @@ -92,6 +93,7 @@ Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: igno Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1 Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf +Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2 Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall [Icons] @@ -104,6 +106,7 @@ Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinec Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1 Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf +Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2 Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server @@ -113,6 +116,7 @@ Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\Archipel Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1 Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf +Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2 [Run] diff --git a/setup.py b/setup.py index 0a1e5c4f..3124f4de 100644 --- a/setup.py +++ b/setup.py @@ -348,7 +348,7 @@ cx_Freeze.setup( "excludes": ["numpy", "Cython", "PySide2", "PIL", "pandas"], "zip_include_packages": ["*"], - "zip_exclude_packages": ["worlds", "kivy"], + "zip_exclude_packages": ["worlds", "kivy", "sc2"], "include_files": [], "include_msvcr": False, "replace_paths": [("*", "")], diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py new file mode 100644 index 00000000..4af3ed6e --- /dev/null +++ b/worlds/sc2wol/Items.py @@ -0,0 +1,169 @@ +from BaseClasses import Item +import typing + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + type: typing.Optional[str] + number: typing.Optional[int] + progression: bool = False + never_exclude: bool = True + quantity: int = 1 + + +class StarcraftWoLItem(Item): + game: str = "Starcraft2WoL" + + def __init__(self, name, advancement: bool = False, code: int = None, player: int = None): + super(StarcraftWoLItem, self).__init__(name, advancement, code, player) + + +def get_full_item_list(): + return item_table + + +SC2WOL_ITEM_ID_OFFSET = 1000 + +item_table = { + "Marine": ItemData(0+SC2WOL_ITEM_ID_OFFSET, "Unit", 0, progression=True), + "Medic": ItemData(1+SC2WOL_ITEM_ID_OFFSET, "Unit", 1, progression=True), + "Firebat": ItemData(2+SC2WOL_ITEM_ID_OFFSET, "Unit", 2, progression=True), + "Marauder": ItemData(3+SC2WOL_ITEM_ID_OFFSET, "Unit", 3, progression=True), + "Reaper": ItemData(4+SC2WOL_ITEM_ID_OFFSET, "Unit", 4, progression=True), + "Hellion": ItemData(5+SC2WOL_ITEM_ID_OFFSET, "Unit", 5, progression=True), + "Vulture": ItemData(6+SC2WOL_ITEM_ID_OFFSET, "Unit", 6, progression=True), + "Goliath": ItemData(7+SC2WOL_ITEM_ID_OFFSET, "Unit", 7, progression=True), + "Diamondback": ItemData(8+SC2WOL_ITEM_ID_OFFSET, "Unit", 8, progression=True), + "Siege Tank": ItemData(9+SC2WOL_ITEM_ID_OFFSET, "Unit", 9, progression=True), + "Medivac": ItemData(10+SC2WOL_ITEM_ID_OFFSET, "Unit", 10, progression=True), + "Wraith": ItemData(11+SC2WOL_ITEM_ID_OFFSET, "Unit", 11, progression=True), + "Viking": ItemData(12+SC2WOL_ITEM_ID_OFFSET, "Unit", 12, progression=True), + "Banshee": ItemData(13+SC2WOL_ITEM_ID_OFFSET, "Unit", 13, progression=True), + "Battlecruiser": ItemData(14+SC2WOL_ITEM_ID_OFFSET, "Unit", 14, progression=True), + "Ghost": ItemData(15+SC2WOL_ITEM_ID_OFFSET, "Unit", 15, progression=True), + "Spectre": ItemData(16+SC2WOL_ITEM_ID_OFFSET, "Unit", 16, progression=True), + "Thor": ItemData(17+SC2WOL_ITEM_ID_OFFSET, "Unit", 17, progression=True), + + "Progressive Infantry Weapon": ItemData (100+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3), + "Progressive Infantry Armor": ItemData (102+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3), + "Progressive Vehicle Weapon": ItemData (103+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3), + "Progressive Vehicle Armor": ItemData (104+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 6, quantity=3), + "Progressive Ship Weapon": ItemData (105+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3), + "Progressive Ship Armor": ItemData (106+SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3), + + "Projectile Accelerator (Bunker)": ItemData (200+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0), + "Neosteel Bunker (Bunker)": ItemData (201+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1), + "Titanium Housing (Missile Turret)": ItemData (202+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2), + "Hellstorm Batteries (Missile Turret)": ItemData (203+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3), + "Advanced Construction (SCV)": ItemData (204+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4), + "Dual-Fusion Welders (SCV)": ItemData (205+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5), + "Fire-Suppression System (Building)": ItemData (206+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6), + "Orbital Command (Building)": ItemData (207+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7), + "Stimpack (Marine)": ItemData (208+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8), + "Combat Shield (Marine)": ItemData (209+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9), + "Advanced Medic Facilities (Medic)": ItemData (210+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10), + "Stabilizer Medpacks (Medic)": ItemData (211+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11), + "Incinerator Gauntlets (Firebat)": ItemData (212+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12), + "Juggernaut Plating (Firebat)": ItemData (213+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13), + "Concussive Shells (Marauder)": ItemData (214+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14), + "Kinetic Foam (Marauder)": ItemData (215+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15), + "U-238 Rounds (Reaper)": ItemData (216+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16), + "G-4 Clusterbomb (Reaper)": ItemData (217+SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17), + + "Twin-Linked Flamethrower (Hellion)": ItemData(300+SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0), + "Thermite Filaments (Hellion)": ItemData(301+SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1), + "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2), + "Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3), + "Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4), + "Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5), + "Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6), + "Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7), + "Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8), + "Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9), + "Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10), + "Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11), + "Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12), + "Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13), + "Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14), + "Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15), + "Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16), + "Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17), + "Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18), + "Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19), + "Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20), + "Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21), + "Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22), + "Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23), + "330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24), + "Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25), + + "Bunker": ItemData (400+SC2WOL_ITEM_ID_OFFSET, "Building", 0, progression=True), + "Missile Turret": ItemData (401+SC2WOL_ITEM_ID_OFFSET, "Building", 1, progression=True), + "Sensor Tower": ItemData (402+SC2WOL_ITEM_ID_OFFSET, "Building", 2), + + "War Pigs": ItemData (500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0), + "Devil Dogs": ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1), + "Hammer Securities": ItemData(502 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 2), + "Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3), + "Siege Breakers": ItemData(504 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 4), + "Hel's Angel": ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5), + "Dusk Wings": ItemData(506 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 6), + "Jackson's Revenge": ItemData(507 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 7), + + "Ultra-Capacitors": ItemData(600 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 0), + "Vanadium Plating": ItemData(601 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 1), + "Orbital Depots": ItemData(602 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 2), + "Micro-Filtering": ItemData(603 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 3), + "Automated Refinery": ItemData(604 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 4), + "Command Center Reactor": ItemData(605 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 5), + "Raven": ItemData(606 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 6), + "Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7), + "Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8), + "Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9), + "Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10), + "Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11), + "Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12), + "Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13), + "Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14), + "Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, progression=True), + "Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16), + "Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17), + "Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18), + "Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19), + + "Zealot": ItemData (700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, progression=True), + "Stalker": ItemData (701 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 1, progression=True), + "High Templar": ItemData (702 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 2, progression=True), + "Dark Templar": ItemData (703 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 3, progression=True), + "Immortal": ItemData (704 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 4, progression=True), + "Colossus": ItemData (705 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 5, progression=True), + "Phoenix": ItemData (706 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 6, progression=True), + "Void Ray": ItemData (707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, progression=True), + "Carrier": ItemData (708 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 8, progression=True), + + "+5 Starting Minerals": ItemData(800+SC2WOL_ITEM_ID_OFFSET, "Minerals", 5, quantity=0), + "+5 Starting Vespene": ItemData(801+SC2WOL_ITEM_ID_OFFSET, "Vespene", 5, quantity=0) +} + +basic_unit: typing.Tuple[str, ...] = ( + 'Marine', + 'Marauder', + 'Firebat', + 'Hellion', + 'Vulture' +) + + +item_name_groups = {"Missions": + {"Beat Liberation Day", "Beat The Outlaws", "Beat Zero Hour", "Beat Evacuation", + "None Outbreak", "Beat Safe Haven", "Beat Haven's Fall", "Beat Smash and Grab", "Beat The Dig", + "Beat The Moebius Factor", "Beat Supernova", "Beat Maw of the Void", "Beat Devil's Playground", + "Beat Welcome to the Jungle", "Beat Breakout", "Beat Ghost of a Chance", + "Beat The Great Train Robbery", "Beat Cutthroat", "Beat Engine of Destruction", + "Beat Media Blitz", "Beat Piercing the Shroud"}} + +filler_items: typing.Tuple[str, ...] = ( + '+5 Starting Minerals', + '+5 Starting Vespene' +) + +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if data.code} \ No newline at end of file diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py new file mode 100644 index 00000000..aa4f070b --- /dev/null +++ b/worlds/sc2wol/Locations.py @@ -0,0 +1,186 @@ +from typing import List, Tuple, Optional, Callable, NamedTuple +from BaseClasses import MultiWorld + +from BaseClasses import Location + +SC2WOL_LOC_ID_OFFSET = 1000 + +class SC2WoLLocation(Location): + game: str = "Starcraft2WoL" + + +class LocationData(NamedTuple): + region: str + name: str + code: Optional[int] + rule: Callable = lambda state: True + + +def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]: + location_table: List[LocationData] = [ + LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100), + LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101), + LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102), + LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103), + LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104), + LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105), + LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106), + LocationData("Liberation Day", "Beat Liberation Day", None), + LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200), + LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201), + LocationData("The Outlaws", "Beat The Outlaws", None), + LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300), + LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301), + LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302), + LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303), + LocationData("Zero Hour", "Beat Zero Hour", None), + LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400), + LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401), + LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402), + LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403), + LocationData("Evacuation", "Beat Evacuation", None), + LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500), + LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501), + LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502), + LocationData("Outbreak", "Beat Outbreak", None), + LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600), + LocationData("Safe Haven", "Beat Safe Haven", None), + LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700), + LocationData("Haven's Fall", "Beat Haven's Fall", None), + LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800), + LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801), + LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802), + LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803), + LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804), + LocationData("Smash and Grab", "Beat Smash and Grab", None), + LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900), + LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901), + LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902), + LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903), + LocationData("The Dig", "Beat The Dig", None), + LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1000), + LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001), + LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002), + LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, + lambda state: state._sc2wol_able_to_rescue(world, player) or True), + LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, + lambda state: state._sc2wol_able_to_rescue(world, player) or True), + LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, + lambda state: state._sc2wol_able_to_rescue(world, player) or True), + LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, + lambda state: state._sc2wol_able_to_rescue(world, player) or True), + LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, + lambda state: state._sc2wol_able_to_rescue(world, player) or True), + LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008), + LocationData("The Moebius Factor", "Beat The Moebius Factor", None), + LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100), + LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), + LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102), + LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103), + LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104), + LocationData("Supernova", "Beat Supernova", None), + LocationData("Maw of the Void", "Maw of the Void: Xel'Naga Vault", SC2WOL_LOC_ID_OFFSET + 1200), + LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201), + LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202), + LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203), + LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204), + LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205), + LocationData("Maw of the Void", "Beat Maw of the Void", None), + LocationData("Devil's Playground", "Devil's Playground: 8000 Minerals", SC2WOL_LOC_ID_OFFSET + 1300), + LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301), + LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302), + LocationData("Devil's Playground", "Beat Devil's Playground", None), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: 7 Canisters", SC2WOL_LOC_ID_OFFSET + 1400), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403), + LocationData("Welcome to the Jungle", "Beat Welcome to the Jungle", None), + LocationData("Breakout", "Breakout: Main Prison", SC2WOL_LOC_ID_OFFSET + 1500), + LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501), + LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502), + LocationData("Breakout", "Beat Breakout", None), + LocationData("Ghost of a Chance", "Ghost of a Chance: Psi-Indoctrinator", SC2WOL_LOC_ID_OFFSET + 1600), + LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601), + LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602), + LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603), + LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604), + LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605), + LocationData("Ghost of a Chance", "Beat Ghost of a Chance", None), + LocationData("The Great Train Robbery", "The Great Train Robbery: 8 Trains", SC2WOL_LOC_ID_OFFSET + 1700, lambda state: state._sc2wol_has_train_killers(world, player)), + LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701), + LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702), + LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703), + LocationData("The Great Train Robbery", "Beat The Great Train Robbery", None, + lambda state: state._sc2wol_has_train_killers(world, player)), + LocationData("Cutthroat", "Cutthroat: Orlan's Planetary", SC2WOL_LOC_ID_OFFSET + 1800), + LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801), + LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802), + LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803), + LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804), + LocationData("Cutthroat", "Beat Cutthroat", None), + LocationData("Engine of Destruction", "Engine of Destruction: Dominion Bases", SC2WOL_LOC_ID_OFFSET + 1900, + lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901), + LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, + lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903), + LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, + lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, + lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + LocationData("Engine of Destruction", "Beat Engine of Destruction", None, + lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + LocationData("Media Blitz", "Media Blitz: Full Upload", SC2WOL_LOC_ID_OFFSET + 2000), + LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001), + LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002), + LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003), + LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004), + LocationData("Media Blitz", "Beat Media Blitz", None), + LocationData("Piercing the Shroud", "Piercing the Shroud: Facility Escape", SC2WOL_LOC_ID_OFFSET + 2100), + LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101), + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102), + LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103), + LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104), + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105), + LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None), + LocationData("Whispers of Doom", "Whispers of Doom: Void Seeker Escape", SC2WOL_LOC_ID_OFFSET + 2200), + LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201), + LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202), + LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203), + LocationData("Whispers of Doom", "Beat Whispers of Doom", None), + LocationData("A Sinister Turn", "A Sinister Turn: Preservers Freed", SC2WOL_LOC_ID_OFFSET + 2300, + lambda state: state._sc2wol_has_protoss_medium_units(world, player)), + LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301), + LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302), + LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, + lambda state: state._sc2wol_has_protoss_common_units(world, player)), + LocationData("A Sinister Turn", "Beat A Sinister Turn", None, + lambda state: state._sc2wol_has_protoss_medium_units(world, player)), + LocationData("Echoes of the Future", "Echoes of the Future: Overmind", SC2WOL_LOC_ID_OFFSET + 2400), + LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401), + LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402), + LocationData("Echoes of the Future", "Beat Echoes of the Future", None), + LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2500), + LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501), + LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2502), + LocationData("In Utter Darkness", "Beat In Utter Darkness", None), + LocationData("Gates of Hell", "Gates of Hell: Nydus Worms", SC2WOL_LOC_ID_OFFSET + 2600), + LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601), + LocationData("Gates of Hell", "Beat Gates of Hell", None), + LocationData("Belly of the Beast", "Belly of the Beast: Extract", SC2WOL_LOC_ID_OFFSET + 2700), + LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701), + LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702), + LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703), + LocationData("Belly of the Beast", "Beat Belly of the Beast", None), + LocationData("Shatter the Sky", "Shatter the Sky: Platform Destroyed", SC2WOL_LOC_ID_OFFSET + 2800), + LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801), + LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802), + LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803), + LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804), + LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805), + LocationData("Shatter the Sky", "Beat Shatter the Sky", None), + LocationData("All-In", "All-In: Victory", None) + ] + + return tuple(location_table) + diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py new file mode 100644 index 00000000..72147372 --- /dev/null +++ b/worlds/sc2wol/LogicMixin.py @@ -0,0 +1,49 @@ +from BaseClasses import MultiWorld +from ..AutoWorld import LogicMixin + + +class SC2WoLLogic(LogicMixin): + def _sc2wol_has_common_unit(self, world: MultiWorld, player: int) -> bool: + return self.has_any({'Marine', 'Marauder', 'Firebat', 'Hellion', 'Vulture'}, player) + + def _sc2wol_has_bunker_unit(self, world: MultiWorld, player: int) -> bool: + return self.has_any({'Marine', 'Marauder'}, player) + + def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool: + return self.has_any({'Viking', 'Wraith', 'Medivac', 'Banshee', 'Hercules'}, player) + + def _sc2wol_has_battlecruiser(self, world: MultiWorld, player: int) -> bool: + return self.has('Battlecruiser', player) + + def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool: + return self.has_any({'Viking', 'Wraith'}, player) + + def _sc2wol_has_mobile_anti_air(self, world: MultiWorld, player: int) -> bool: + return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(world, player) + + def _sc2wol_has_anti_air(self, world: MultiWorld, player: int) -> bool: + return self.has('Missile Turret', player) or self._sc2wol_has_mobile_anti_air(world, player) + + def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool: + return (self.has_any({'Siege Tank', 'Vulture'}, player) or + self.has('Bunker', player) and self._sc2wol_has_bunker_unit(world, player)) and \ + self._sc2wol_has_anti_air(world, player) + + def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool: + return (self.has_any({'Siege Tank', 'Diamondback'}, player) or + self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) + + def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool: + return self.has_any({'Medivac', 'Hercules', 'Raven', 'Orbital Strike'}, player) + + def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool: + return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) + + def _sc2wol_has_protoss_medium_units(self, world: MultiWorld, player: int) -> bool: + return self._sc2wol_has_protoss_common_units(world, player) and \ + self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) + + def _sc2wol_cleared_missions(self, world: MultiWorld, player: int, mission_count: int) -> bool: + return self.has_group("Missions", player, mission_count) + + diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py new file mode 100644 index 00000000..6a06872c --- /dev/null +++ b/worlds/sc2wol/Options.py @@ -0,0 +1,48 @@ +from typing import Dict +from BaseClasses import MultiWorld +from Options import Choice, Option + +class GameDifficulty(Choice): + """The difficulty of the campaign, affects enemy AI, starting units, and game speed.""" + display_name = "Game Difficulty" + option_casual = 0 + option_normal = 1 + option_hard = 2 + option_brutal = 3 + +class UpgradeBonus(Choice): + """Determines what lab upgrade to use, whether it is Ultra-Capacitors which boost attack speed with every weapon upgrade + or Vanadium Plating which boosts life with every armor upgrade.""" + display_name = "Upgrade Bonus" + option_ultra_capacitors = 0 + option_vanadium_plating = 1 + +class BunkerUpgrade(Choice): + """Determines what bunker lab upgrade to use, whether it is Shrike Turret which outfits bunkers with an automated turret or + Fortified Bunker which boosts the life of bunkers.""" + display_name = "Bunker Upgrade" + option_shrike_turret = 0 + option_fortified_bunker = 1 + +class AllInMap(Choice): + """Determines what verion of All-In (final map) that will be generated for the campaign.""" + display_name = "All In Map" + option_ground = 0 + option_air = 1 + + +# noinspection PyTypeChecker +sc2wol_options: Dict[str, Option] = { + "game_difficulty": GameDifficulty, + "upgrade_bonus": UpgradeBonus, + "bunker_upgrade": BunkerUpgrade, + "all_in_map": AllInMap, +} + +def get_option_value(world: MultiWorld, player: int, name: str) -> int: + option = getattr(world, name, None) + + if option == None: + return 0 + + return int(option[player].value) \ No newline at end of file diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py new file mode 100644 index 00000000..0f0c408e --- /dev/null +++ b/worlds/sc2wol/Regions.py @@ -0,0 +1,162 @@ +from typing import List, Set, Dict, Tuple, Optional, Callable +from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType +from .Locations import LocationData + + +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") + ] + + if __debug__: + throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) + + world.regions += regions + + names: Dict[str, int] = {} + + connect(world, player, names, 'Menu', 'Liberation Day'), + connect(world, player, names, 'Liberation Day', 'The Outlaws', + lambda state: state._sc2wol_has_common_unit(world, player)), + connect(world, player, names, 'The Outlaws', 'Zero Hour'), + connect(world, player, names, 'Zero Hour', 'Evacuation', + lambda state: state._sc2wol_has_anti_air(world, player)), + connect(world, player, names, 'Evacuation', 'Outbreak'), + connect(world, player, names, "Outbreak", "Safe Haven", + lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + state._sc2wol_cleared_missions(world, player, 7)), + connect(world, player, names, "Outbreak", "Haven's Fall", + lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + state._sc2wol_cleared_missions(world, player, 7)), + connect(world, player, names, 'Zero Hour', 'Smash and Grab', + lambda state: state._sc2wol_has_anti_air(world, player)), + connect(world, player, names, 'Smash and Grab', 'The Dig', + lambda state: state._sc2wol_cleared_missions(world, player, 8) and + state._sc2wol_has_heavy_defense(world, player)), + connect(world, player, names, 'The Dig', 'The Moebius Factor', + lambda state: state._sc2wol_cleared_missions(world, player, 11) and + state._sc2wol_has_air(world, player)), + connect(world, player, names, 'The Moebius Factor', 'Supernova', + lambda state: state._sc2wol_cleared_missions(world, player, 14)), + connect(world, player, names, 'Supernova', 'Maw of the Void'), + connect(world, player, names, 'Zero Hour', "Devil's Playground", + lambda state: state._sc2wol_cleared_missions(world, player, 4)), + connect(world, player, names, "Devil's Playground", 'Welcome to the Jungle'), + connect(world, player, names, "Welcome to the Jungle", 'Breakout', + lambda state: state._sc2wol_cleared_missions(world, player, 8)), + connect(world, player, names, "Welcome to the Jungle", 'Ghost of a Chance', + lambda state: state._sc2wol_cleared_missions(world, player, 8)), + connect(world, player, names, "Zero Hour", 'The Great Train Robbery', + lambda state: state._sc2wol_cleared_missions(world, player, 6)), + connect(world, player, names, 'The Great Train Robbery', 'Cutthroat', + lambda state: state.has("Beat The Great Train Robbery", player)), + connect(world, player, names, 'Cutthroat', 'Engine of Destruction', + lambda state: state.has("Beat The Great Train Robbery", player)), + connect(world, player, names, 'Engine of Destruction', 'Media Blitz', + lambda state: state.has("Beat Engine of Destruction", player)), + connect(world, player, names, 'Media Blitz', 'Piercing the Shroud'), + connect(world, player, names, 'The Dig', 'Whispers of Doom',), + connect(world, player, names, 'Whispers of Doom', 'A Sinister Turn'), + connect(world, player, names, 'A Sinister Turn', 'Echoes of the Future', + lambda state: state.has("Beat A Sinister Turn", player)), + connect(world, player, names, 'Echoes of the Future', 'In Utter Darkness'), + connect(world, player, names, 'Maw of the Void', 'Gates of Hell'), + connect(world, player, names, 'Gates of Hell', 'Belly of the Beast'), + connect(world, player, names, 'Gates of Hell', 'Shatter the Sky'), + connect(world, player, names, 'Gates of Hell', 'All-In', + lambda state: state.has('Beat Gates of Hell', player) or state.has('Beat Shatter the Sky', player)) + + +def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): + existingRegions = set() + + for region in regions: + existingRegions.add(region.name) + + if (regionNames - existingRegions): + raise Exception("Starcraft: the following regions are used in locations: {}, but no such region exists".format(regionNames - existingRegions)) + + +def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location: + location = Location(player, location_data.name, location_data.code, region) + location.access_rule = location_data.rule + + if id is None: + location.event = True + location.locked = True + + location_cache.append(location) + + return location + + +def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], location_cache: List[Location], name: str) -> Region: + region = Region(name, RegionType.Generic, name, player) + region.world = world + + if name in locations_per_region: + for location_data in locations_per_region[name]: + location = create_location(player, location_data, region, location_cache) + region.locations.append(location) + + return region + + +def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str, rule: Optional[Callable] = None): + sourceRegion = world.get_region(source, player) + targetRegion = world.get_region(target, player) + + if target not in used_names: + used_names[target] = 1 + name = target + else: + used_names[target] += 1 + name = target + (' ' * used_names[target]) + + connection = Entrance(player, name, sourceRegion) + + if rule: + connection.access_rule = rule + + sourceRegion.exits.append(connection) + connection.connect(targetRegion) + + +def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]: + per_region: Dict[str, List[LocationData]] = {} + + for location in locations: + per_region.setdefault(location.region, []).append(location) + + return per_region diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py new file mode 100644 index 00000000..70bcafee --- /dev/null +++ b/worlds/sc2wol/__init__.py @@ -0,0 +1,173 @@ +import typing + +from typing import List, Set, Tuple +from BaseClasses import Item, MultiWorld, Location, Tutorial +from ..AutoWorld import World, WebWorld +from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \ + basic_unit +from .Locations import get_locations +from .Regions import create_regions +from .Options import sc2wol_options, get_option_value +from .LogicMixin import SC2WoLLogic +from ..AutoWorld import World + + +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() + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = {location.name: location.code for location in get_locations(None, None)} + options = sc2wol_options + + item_name_groups = item_name_groups + locked_locations: typing.List[str] + location_cache: typing.List[Location] + + def _get_sc2wol_data(self): + return {} + + def __init__(self, world: MultiWorld, player: int): + super(SC2WoLWorld, self).__init__(world, player) + self.location_cache = [] + self.locked_locations = [] + + def _create_items(self, name: str): + data = get_full_item_list()[name] + return [self.create_item(name)] * data.quantity + + def create_item(self, name: str) -> Item: + data = get_full_item_list()[name] + return StarcraftWoLItem(name, data.progression, data.code, self.player) + + def create_regions(self): + create_regions(self.world, self.player, get_locations(self.world, self.player), + self.location_cache) + + 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) + + pool = get_item_pool(self.world, self.player, excluded_items) + + fill_item_pool_with_dummy_items(self, self.world, self.player, self.locked_locations, self.location_cache, pool) + + self.world.itempool += pool + + def set_rules(self): + setup_events(self.world, self.player, self.locked_locations, self.location_cache) + + self.world.completion_condition[self.player] = lambda state: state.has('All-In: Victory', self.player) + + def get_filler_item_name(self) -> str: + return self.world.random.choice(filler_items) + + def fill_slot_data(self): + slot_data = self._get_sc2wol_data() + for option_name in sc2wol_options: + option = getattr(self.world, option_name)[self.player] + if slot_data.get(option_name, None) is None and type(option.value) in {str, int}: + slot_data[option_name] = int(option.value) + return slot_data + + +def setup_events(world: MultiWorld, player: int, locked_locations: typing.List[str], location_cache: typing.List[Location]): + for location in location_cache: + if location.address == None: + item = Item(location.name, True, None, player) + + locked_locations.append(location.name) + + location.place_locked_item(item) + + +def get_excluded_items(self: SC2WoLWorld, world: MultiWorld, player: int) -> Set[str]: + excluded_items: Set[str] = set() + + if get_option_value(world, player, "upgrade_bonus") == 1: + excluded_items.add("Ultra-Capacitors") + else: + excluded_items.add("Vanadium Plating") + + if get_option_value(world, player, "bunker_upgrade") == 1: + excluded_items.add("Shrike Turret") + else: + excluded_items.add("Fortified Bunker") + + for item in world.precollected_items[player]: + excluded_items.add(item.name) + + return excluded_items + + +def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): + 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) + if not local_basic_unit: + raise Exception("At least one basic unit must be local") + + assign_starter_item(world, player, excluded_items, locked_locations, 'Liberation Day: First Statue', + 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, ...]): + + item_name = world.random.choice(item_list) + + excluded_items.add(item_name) + + item = create_item_with_correct_settings(world, player, item_name) + + world.get_location(location, player).place_locked_item(item) + + locked_locations.append(location) + + +def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]: + pool: List[Item] = [] + + for name, data in item_table.items(): + if name not in excluded_items: + for _ in range(data.quantity): + item = create_item_with_correct_settings(world, player, name) + pool.append(item) + + return pool + +def fill_item_pool_with_dummy_items(self: SC2WoLWorld, world: 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(world, player, self.get_filler_item_name()) + pool.append(item) + + +def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) -> Item: + data = item_table[name] + + item = Item(name, data.progression, data.code, player) + item.never_exclude = data.never_exclude + + if not item.advancement: + return item + + return item \ No newline at end of file diff --git a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md new file mode 100644 index 00000000..6cf3fc7f --- /dev/null +++ b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md @@ -0,0 +1,33 @@ +# Starcraft 2 Wings of Liberty + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is +always able to be completed, but because of the item shuffle the player may need to access certain areas before they +would in the vanilla game. All rings and spells are also randomized into those item locations, therefore you can no +longer craft them at the alchemist + +## What is the goal of Starcraft 2 when randomized? + +The goal remains unchanged. Beat the final mission All In. + +## What items and locations get shuffled? + +Unit unlocks, upgrade unlocks, armory upgrades, laboratory researches, and mercenary unlocks can be shuffled, and all +bonus objectives, side missions, mission completions are now locations that can contain these items. + +## Which items can be in another player's world? + +Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit +certain items to your own world. + +## When the player receives an item, what happens? + +When the player receives an item, they will receive a message through their text client and in game if currently playing + a mission. They will immediately be able to use that unlock/upgrade. + diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md new file mode 100644 index 00000000..adce0e08 --- /dev/null +++ b/worlds/sc2wol/docs/setup_en.md @@ -0,0 +1,33 @@ +# Starcraft 2 Wings of Liberty Randomizer Setup Guide + +## Required Software + +- [Starcraft 2](https://starcraft2.com/en-us/) +- [Starcraft 2 AP Client](https://github.com/ArchipelagoMW/Archipelago) +- [Starcraft 2 AP Maps and Data](https://github.com/TheCondor07/Starcraft2ArchipelagoData) + +## General Concept + +Starcraft 2 AP Client launches a custom version of Starcraft 2 running modified Wings of Liberty campaign maps + to allow for randomization of the items + +## Installation Procedures + +Download latest release on [Starcraft 2 Archipelago Data Releases](https://github.com/TheCondor07/Starcraft2ArchipelagoData/releases) you +can find the .zip files on the releases page. Download the zip then extract the zip to the +folder where your Starcraft 2 game is installed. The just run ArchipelagoStarcraftClient.exe to start the client to + connect to a Multiworld Game. + +## Joining a MultiWorld Game + +1. Run ArchipelagoStarcraftClient.exe +2. Type in /connect [sever ip] +3. Insert slot name and password as prompted +4. Once connected, use /unfinished to find what missions you can play and '/play [mission id]' to launch a mission. For +new games under default settings the first mission available will always be Liberation Day[1] playable using the command +'/play 1' + +## Where do I get a config file? + +The [Player Settings](https://archipelago.gg/games/Starcraft%202%20Wings%20of%20Liberty/player-settings) page on the website allows you to +configure your personal settings and export them into a config file diff --git a/worlds/sc2wol/requirements.txt b/worlds/sc2wol/requirements.txt new file mode 100644 index 00000000..9a2bec4f --- /dev/null +++ b/worlds/sc2wol/requirements.txt @@ -0,0 +1,3 @@ +nest-asyncio >= 1.5.5 +six >= 1.16.0 +apsc2 >= 5.5 \ No newline at end of file