diff --git a/Starcraft2Client.py b/Starcraft2Client.py
index cdcdb39a..87b50d35 100644
--- a/Starcraft2Client.py
+++ b/Starcraft2Client.py
@@ -1,1049 +1,11 @@
from __future__ import annotations
-import asyncio
-import copy
-import ctypes
-import logging
-import multiprocessing
-import os.path
-import re
-import sys
-import typing
-import queue
-import zipfile
-import io
-from pathlib import Path
+import ModuleUpdate
+ModuleUpdate.update()
-# CommonClient import first to trigger ModuleUpdater
-from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
-from Utils import init_logging, is_windows
+from worlds.sc2wol.Client import launch
+import Utils
if __name__ == "__main__":
- init_logging("SC2Client", exception_logger="Client")
-
-logger = logging.getLogger("Client")
-sc2_logger = logging.getLogger("Starcraft2")
-
-import nest_asyncio
-from worlds._sc2common import bot
-from worlds._sc2common.bot.data import Race
-from worlds._sc2common.bot.main import run_game
-from worlds._sc2common.bot.player import Bot
-from worlds.sc2wol import SC2WoLWorld
-from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
-from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
-from worlds.sc2wol.MissionTables import lookup_id_to_mission
-from worlds.sc2wol.Regions import MissionInfo
-
-import colorama
-from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser
-from MultiServer import mark_raw
-
-nest_asyncio.apply()
-max_bonus: int = 8
-victory_modulo: int = 100
-
-
-class StarcraftClientProcessor(ClientCommandProcessor):
- ctx: SC2Context
-
- def _cmd_difficulty(self, difficulty: str = "") -> bool:
- """Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
- options = difficulty.split()
- num_options = len(options)
-
- if num_options > 0:
- difficulty_choice = options[0].lower()
- if difficulty_choice == "casual":
- self.ctx.difficulty_override = 0
- elif difficulty_choice == "normal":
- self.ctx.difficulty_override = 1
- elif difficulty_choice == "hard":
- self.ctx.difficulty_override = 2
- elif difficulty_choice == "brutal":
- self.ctx.difficulty_override = 3
- else:
- self.output("Unable to parse difficulty '" + options[0] + "'")
- return False
-
- self.output("Difficulty set to " + options[0])
- return True
-
- else:
- if self.ctx.difficulty == -1:
- self.output("Please connect to a seed before checking difficulty.")
- else:
- self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][self.ctx.difficulty])
- self.output("To change the difficulty, add the name of the difficulty after the command.")
- return False
-
- def _cmd_disable_mission_check(self) -> bool:
- """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
- the next mission in a chain the other player is doing."""
- self.ctx.missions_unlocked = True
- sc2_logger.info("Mission check has been disabled")
- return True
-
- 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])
-
- self.ctx.play_mission(mission_number)
-
- else:
- sc2_logger.info(
- "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
- return False
-
- return True
-
- def _cmd_available(self) -> bool:
- """Get what missions are currently available to play"""
-
- request_available_missions(self.ctx)
- 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)
- return True
-
- @mark_raw
- def _cmd_set_path(self, path: str = '') -> bool:
- """Manually set the SC2 install directory (if the automatic detection fails)."""
- if path:
- os.environ["SC2PATH"] = path
- is_mod_installed_correctly()
- return True
- else:
- sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
- return False
-
- def _cmd_download_data(self) -> bool:
- """Download the most recent release of the necessary files for playing SC2 with
- Archipelago. Will overwrite existing files."""
- if "SC2PATH" not in os.environ:
- check_game_install_path()
-
- if os.path.exists(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt"):
- with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "r") as f:
- current_ver = f.read()
- else:
- current_ver = None
-
- tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData',
- current_version=current_ver, force_download=True)
-
- if tempzip != '':
- try:
- zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
- sc2_logger.info(f"Download complete. Version {version} installed.")
- with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "w") as f:
- f.write(version)
- finally:
- os.remove(tempzip)
- else:
- sc2_logger.warning("Download aborted/failed. Read the log for more information.")
- return False
- return True
-
-
-class SC2Context(CommonContext):
- command_processor = StarcraftClientProcessor
- game = "Starcraft 2 Wings of Liberty"
- items_handling = 0b111
- difficulty = -1
- all_in_choice = 0
- mission_order = 0
- mission_req_table: typing.Dict[str, MissionInfo] = {}
- final_mission: int = 29
- announcements = queue.Queue()
- sc2_run_task: typing.Optional[asyncio.Task] = None
- missions_unlocked: bool = False # allow launching missions ignoring requirements
- current_tooltip = None
- last_loc_list = None
- difficulty_override = -1
- mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
- last_bot: typing.Optional[ArchipelagoBot] = None
-
- def __init__(self, *args, **kwargs):
- super(SC2Context, self).__init__(*args, **kwargs)
- self.raw_text_parser = RawJSONtoTextParser(self)
-
- async def server_auth(self, password_requested: bool = False):
- if password_requested and not self.password:
- await super(SC2Context, self).server_auth(password_requested)
- await self.get_username()
- 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"]
- slot_req_table = args["slot_data"]["mission_req"]
- # Maintaining backwards compatibility with older slot data
- self.mission_req_table = {
- mission: MissionInfo(
- **{field: value for field, value in mission_info.items() if field in MissionInfo._fields}
- )
- for mission, mission_info in slot_req_table.items()
- }
- self.mission_order = args["slot_data"].get("mission_order", 0)
- self.final_mission = args["slot_data"].get("final_mission", 29)
-
- self.build_location_to_mission_mapping()
-
- # Looks for the required maps and mods for SC2. Runs check_game_install_path.
- maps_present = is_mod_installed_correctly()
- if os.path.exists(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt"):
- with open(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt", "r") as f:
- current_ver = f.read()
- if is_mod_update_available("TheCondor07", "Starcraft2ArchipelagoData", current_ver):
- sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
- elif maps_present:
- sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). "
- "Run /download_data to update them.")
-
-
- def on_print_json(self, args: dict):
- # goes to this world
- if "receiving" in args and self.slot_concerns_self(args["receiving"]):
- relevant = True
- # found in this world
- elif "item" in args and self.slot_concerns_self(args["item"].player):
- relevant = True
- # not related
- else:
- relevant = False
-
- if relevant:
- self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"])))
-
- super(SC2Context, self).on_print_json(args)
-
- def run_gui(self):
- from kvui import GameManager, HoverBehavior, ServerToolTip
- from kivy.app import App
- from kivy.clock import Clock
- from kivy.uix.tabbedpanel import TabbedPanelItem
- from kivy.uix.gridlayout import GridLayout
- from kivy.lang import Builder
- from kivy.uix.label import Label
- from kivy.uix.button import Button
- from kivy.uix.floatlayout import FloatLayout
- from kivy.properties import StringProperty
-
- class HoverableButton(HoverBehavior, Button):
- pass
-
- class MissionButton(HoverableButton):
- tooltip_text = StringProperty("Test")
- ctx: SC2Context
-
- def __init__(self, *args, **kwargs):
- super(HoverableButton, self).__init__(*args, **kwargs)
- self.layout = FloatLayout()
- self.popuplabel = ServerToolTip(text=self.text)
- self.layout.add_widget(self.popuplabel)
-
- def on_enter(self):
- self.popuplabel.text = self.tooltip_text
-
- if self.ctx.current_tooltip:
- App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
-
- if self.tooltip_text == "":
- self.ctx.current_tooltip = None
- else:
- App.get_running_app().root.add_widget(self.layout)
- self.ctx.current_tooltip = self.layout
-
- def on_leave(self):
- self.ctx.ui.clear_tooltip()
-
- @property
- def ctx(self) -> CommonContext:
- return App.get_running_app().ctx
-
- class MissionLayout(GridLayout):
- pass
-
- class MissionCategory(GridLayout):
- pass
-
- class SC2Manager(GameManager):
- logging_pairs = [
- ("Client", "Archipelago"),
- ("Starcraft2", "Starcraft2"),
- ]
- base_title = "Archipelago Starcraft 2 Client"
-
- mission_panel = None
- last_checked_locations = {}
- mission_id_to_button = {}
- launching: typing.Union[bool, int] = False # if int -> mission ID
- refresh_from_launching = True
- first_check = True
- ctx: SC2Context
-
- def __init__(self, ctx):
- super().__init__(ctx)
-
- def clear_tooltip(self):
- if self.ctx.current_tooltip:
- App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
-
- self.ctx.current_tooltip = None
-
- def build(self):
- container = super().build()
-
- panel = TabbedPanelItem(text="Starcraft 2 Launcher")
- self.mission_panel = panel.content = MissionLayout()
-
- self.tabs.add_widget(panel)
-
- Clock.schedule_interval(self.build_mission_table, 0.5)
-
- return container
-
- def build_mission_table(self, dt):
- if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
- not self.refresh_from_launching)) or self.first_check:
- self.refresh_from_launching = True
-
- self.mission_panel.clear_widgets()
- if self.ctx.mission_req_table:
- self.last_checked_locations = self.ctx.checked_locations.copy()
- self.first_check = False
-
- self.mission_id_to_button = {}
- categories = {}
- available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
-
- # separate missions into categories
- for mission in self.ctx.mission_req_table:
- if not self.ctx.mission_req_table[mission].category in categories:
- categories[self.ctx.mission_req_table[mission].category] = []
-
- categories[self.ctx.mission_req_table[mission].category].append(mission)
-
- for category in categories:
- category_panel = MissionCategory()
- if category.startswith('_'):
- category_display_name = ''
- else:
- category_display_name = category
- category_panel.add_widget(
- Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1))
-
- for mission in categories[category]:
- text: str = mission
- tooltip: str = ""
- mission_id: int = self.ctx.mission_req_table[mission].id
- # Map has uncollected locations
- if mission in unfinished_missions:
- text = f"[color=6495ED]{text}[/color]"
- elif mission in available_missions:
- text = f"[color=FFFFFF]{text}[/color]"
- # Map requirements not met
- else:
- text = f"[color=a9a9a9]{text}[/color]"
- tooltip = f"Requires: "
- if self.ctx.mission_req_table[mission].required_world:
- tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
- req_mission in
- self.ctx.mission_req_table[mission].required_world)
-
- if self.ctx.mission_req_table[mission].number:
- tooltip += " and "
- if self.ctx.mission_req_table[mission].number:
- tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed"
- remaining_location_names: typing.List[str] = [
- self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
- if loc in self.ctx.missing_locations]
-
- if mission_id == self.ctx.final_mission:
- if mission in available_missions:
- text = f"[color=FFBC95]{mission}[/color]"
- else:
- text = f"[color=D0C0BE]{mission}[/color]"
- if tooltip:
- tooltip += "\n"
- tooltip += "Final Mission"
-
- if remaining_location_names:
- if tooltip:
- tooltip += "\n"
- tooltip += f"Uncollected locations:\n"
- tooltip += "\n".join(remaining_location_names)
-
- mission_button = MissionButton(text=text, size_hint_y=None, height=50)
- mission_button.tooltip_text = tooltip
- mission_button.bind(on_press=self.mission_callback)
- self.mission_id_to_button[mission_id] = mission_button
- category_panel.add_widget(mission_button)
-
- category_panel.add_widget(Label(text=""))
- self.mission_panel.add_widget(category_panel)
-
- elif self.launching:
- self.refresh_from_launching = False
-
- self.mission_panel.clear_widgets()
- self.mission_panel.add_widget(Label(text="Launching Mission: " +
- lookup_id_to_mission[self.launching]))
- if self.ctx.ui:
- self.ctx.ui.clear_tooltip()
-
- def mission_callback(self, button):
- if not self.launching:
- mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button)
- self.ctx.play_mission(mission_id)
- self.launching = mission_id
- Clock.schedule_once(self.finish_launching, 10)
-
- def finish_launching(self, dt):
- self.launching = False
-
- self.ui = SC2Manager(self)
- self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
- import pkgutil
- data = pkgutil.get_data(SC2WoLWorld.__module__, "Starcraft2.kv").decode()
- Builder.load_string(data)
-
- async def shutdown(self):
- await super(SC2Context, self).shutdown()
- if self.last_bot:
- self.last_bot.want_close = True
- if self.sc2_run_task:
- self.sc2_run_task.cancel()
-
- def play_mission(self, mission_id: int):
- if self.missions_unlocked or \
- is_mission_available(self, mission_id):
- if self.sc2_run_task:
- if not self.sc2_run_task.done():
- sc2_logger.warning("Starcraft 2 Client is still running!")
- self.sc2_run_task.cancel() # doesn't actually close the game, just stops the python task
- if self.slot is None:
- sc2_logger.warning("Launching Mission without Archipelago authentication, "
- "checks will not be registered to server.")
- self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id),
- name="Starcraft 2 Launch")
- else:
- sc2_logger.info(
- f"{lookup_id_to_mission[mission_id]} is not currently unlocked. "
- f"Use /unfinished or /available to see what is available.")
-
- def build_location_to_mission_mapping(self):
- mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
- mission_info.id: set() for mission_info in self.mission_req_table.values()
- }
-
- for loc in self.server_locations:
- mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo)
- mission_id_to_location_ids[mission_id].add(objective)
- self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in
- mission_id_to_location_ids.items()}
-
- def locations_for_mission(self, mission: str):
- mission_id: int = self.mission_req_table[mission].id
- objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id]
- for objective in objectives:
- yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective
-
-
-async def main():
- multiprocessing.freeze_support()
- parser = get_base_parser()
- parser.add_argument('--name', default=None, help="Slot Name to connect as.")
- args = parser.parse_args()
-
- ctx = SC2Context(args.connect, args.password)
- ctx.auth = args.name
- if ctx.server_task is None:
- ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
-
- if gui_enabled:
- ctx.run_gui()
- ctx.run_cli()
-
- await ctx.exit_event.wait()
-
- await ctx.shutdown()
-
-
-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"
-]
-
-wol_default_categories = [
- "Mar Sara", "Mar Sara", "Mar Sara", "Colonist", "Colonist", "Colonist", "Colonist",
- "Artifact", "Artifact", "Artifact", "Artifact", "Artifact", "Covert", "Covert", "Covert", "Covert",
- "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy",
- "Char", "Char", "Char", "Char"
-]
-wol_default_category_names = [
- "Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char"
-]
-
-
-def calculate_items(items: typing.List[NetworkItem]) -> typing.List[int]:
- network_item: NetworkItem
- accumulators: typing.List[int] = [0 for _ in type_flaggroups]
-
- for network_item in items:
- name: str = lookup_id_to_name[network_item.item]
- item_data: ItemData = item_table[name]
-
- # exists exactly once
- if item_data.quantity == 1:
- accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number
-
- # exists multiple times
- elif item_data.type == "Upgrade":
- accumulators[type_flaggroups[item_data.type]] += 1 << item_data.number
-
- # sum
- else:
- accumulators[type_flaggroups[item_data.type]] += item_data.number
-
- return accumulators
-
-
-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: SC2Context, mission_id: int):
- sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
-
- with DllDirectory(None):
- run_game(bot.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
- name="Archipelago", fullscreen=True)], realtime=True)
-
-
-class ArchipelagoBot(bot.bot_ai.BotAI):
- game_running: bool = False
- mission_completed: bool = False
- boni: typing.List[bool]
- setup_done: bool
- ctx: SC2Context
- mission_id: int
- want_close: bool = False
- can_read_game = False
-
- last_received_update: int = 0
-
- def __init__(self, ctx: SC2Context, mission_id):
- self.setup_done = False
- self.ctx = ctx
- self.ctx.last_bot = self
- self.mission_id = mission_id
- self.boni = [False for _ in range(max_bonus)]
-
- super(ArchipelagoBot, self).__init__()
-
- async def on_step(self, iteration: int):
- if self.want_close:
- self.want_close = False
- await self._client.leave()
- return
- game_state = 0
- if not self.setup_done:
- self.setup_done = True
- start_items = calculate_items(self.ctx.items_received)
- if self.ctx.difficulty_override >= 0:
- difficulty = calc_difficulty(self.ctx.difficulty_override)
- else:
- 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, start_items[10]))
- self.last_received_update = len(self.ctx.items_received)
-
- else:
- if not self.ctx.announcements.empty():
- message = self.ctx.announcements.get(timeout=1)
- await self.chat_send("SendMessage " + message)
- self.ctx.announcements.task_done()
-
- # Archipelago reads the health
- for unit in self.all_own_units():
- if unit.health_max == 38281:
- game_state = int(38281 - unit.health)
- self.can_read_game = True
-
- if iteration == 160 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 self.can_read_game:
- if game_state & (1 << 1) and not self.mission_completed:
- if self.mission_id != self.ctx.final_mission:
- print("Mission Completed")
- await self.ctx.send_msgs(
- [{"cmd": 'LocationChecks',
- "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * 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
-
- for x, completed in enumerate(self.boni):
- if not completed and game_state & (1 << (x + 2)):
- await self.ctx.send_msgs(
- [{"cmd": 'LocationChecks',
- "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}])
- self.boni[x] = True
-
- else:
- await self.chat_send("LostConnection - Lost connection to game.")
-
-
-def request_unfinished_missions(ctx: SC2Context):
- if ctx.mission_req_table:
- message = "Unfinished Missions: "
- unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
- unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table)
-
- _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
-
- # Removing All-In from location pool
- final_mission = lookup_id_to_mission[ctx.final_mission]
- if final_mission in unfinished_missions.keys():
- message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message
- if unfinished_missions[final_mission] == -1:
- unfinished_missions.pop(final_mission)
-
- message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
- mark_up_objectives(
- f"[{len(unfinished_missions[mission])}/"
- f"{sum(1 for _ in ctx.locations_for_mission(mission))}]",
- ctx, unfinished_locations, mission)
- for mission in unfinished_missions)
-
- if ctx.ui:
- ctx.ui.log_panels['All'].on_message_markup(message)
- ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
- else:
- sc2_logger.info(message)
- else:
- sc2_logger.warning("No mission table found, you are likely not connected to a server.")
-
-
-def calc_unfinished_missions(ctx: SC2Context, unlocks=None):
- unfinished_missions = []
- locations_completed = []
-
- if not unlocks:
- unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
-
- available_missions = calc_available_missions(ctx, unlocks)
-
- for name in available_missions:
- objectives = set(ctx.locations_for_mission(name))
- if objectives:
- objectives_completed = ctx.checked_locations & objectives
- if len(objectives_completed) < len(objectives):
- unfinished_missions.append(name)
- locations_completed.append(objectives_completed)
-
- else: # infer that this is the final mission as it has no objectives
- unfinished_missions.append(name)
- locations_completed.append(-1)
-
- return available_missions, dict(zip(unfinished_missions, locations_completed))
-
-
-def is_mission_available(ctx: SC2Context, mission_id_to_check):
- unfinished_missions = calc_available_missions(ctx)
-
- return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions)
-
-
-def mark_up_mission_name(ctx: SC2Context, mission, unlock_table):
- """Checks if the mission is required for game completion and adds '*' to the name to mark that."""
-
- if ctx.mission_req_table[mission].completion_critical:
- if ctx.ui:
- message = "[color=AF99EF]" + mission + "[/color]"
- else:
- message = "*" + mission + "*"
- else:
- message = mission
-
- if ctx.ui:
- unlocks = unlock_table[mission]
-
- if len(unlocks) > 0:
- pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: "
- pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks)
- pre_message += f"]"
- message = pre_message + message + "[/ref]"
-
- return message
-
-
-def mark_up_objectives(message, ctx, unfinished_locations, mission):
- formatted_message = message
-
- if ctx.ui:
- locations = unfinished_locations[mission]
-
- pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|"
- pre_message += "
".join(location for location in locations)
- pre_message += f"]"
- formatted_message = pre_message + message + "[/ref]"
-
- return formatted_message
-
-
-def request_available_missions(ctx: SC2Context):
- if ctx.mission_req_table:
- message = "Available Missions: "
-
- # Initialize mission unlock table
- unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
-
- missions = calc_available_missions(ctx, unlocks)
- message += \
- ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}"
- f"[{ctx.mission_req_table[mission].id}]"
- for mission in missions)
-
- if ctx.ui:
- ctx.ui.log_panels['All'].on_message_markup(message)
- ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
- else:
- sc2_logger.info(message)
- else:
- sc2_logger.warning("No mission table found, you are likely not connected to a server.")
-
-
-def calc_available_missions(ctx: SC2Context, unlocks=None):
- available_missions = []
- missions_complete = 0
-
- # Get number of missions completed
- for loc in ctx.checked_locations:
- if loc % victory_modulo == 0:
- missions_complete += 1
-
- for name in ctx.mission_req_table:
- # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
- if unlocks:
- for unlock in ctx.mission_req_table[name].required_world:
- unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name)
-
- if mission_reqs_completed(ctx, name, missions_complete):
- available_missions.append(name)
-
- return available_missions
-
-
-def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int):
- """Returns a bool signifying if the mission has all requirements complete and can be done
-
- Arguments:
- ctx -- instance of SC2Context
- locations_to_check -- the mission string name to check
- missions_complete -- an int of how many missions have been completed
- mission_path -- a list of missions that have already been checked
-"""
- if len(ctx.mission_req_table[mission_name].required_world) >= 1:
- # A check for when the requirements are being or'd
- or_success = False
-
- # Loop through required missions
- for req_mission in ctx.mission_req_table[mission_name].required_world:
- req_success = True
-
- # Check if required mission has been completed
- if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id *
- victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations:
- if not ctx.mission_req_table[mission_name].or_requirements:
- return False
- else:
- req_success = False
-
- # Grid-specific logic (to avoid long path checks and infinite recursion)
- if ctx.mission_order in (3, 4):
- if req_success:
- return True
- else:
- if req_mission is ctx.mission_req_table[mission_name].required_world[-1]:
- return False
- else:
- continue
-
- # Recursively check required mission to see if it's requirements are met, in case !collect has been done
- # Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion
- if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
- if not ctx.mission_req_table[mission_name].or_requirements:
- return False
- else:
- req_success = False
-
- # If requirement check succeeded mark or as satisfied
- if ctx.mission_req_table[mission_name].or_requirements and req_success:
- or_success = True
-
- if ctx.mission_req_table[mission_name].or_requirements:
- # Return false if or requirements not met
- if not or_success:
- return False
-
- # Check number of missions
- if missions_complete >= ctx.mission_req_table[mission_name].number:
- return True
- else:
- return False
- else:
- return True
-
-
-def initialize_blank_mission_dict(location_table):
- unlocks = {}
-
- for mission in list(location_table):
- unlocks[mission] = []
-
- return unlocks
-
-
-def check_game_install_path() -> bool:
- # First thing: go to the default location for ExecuteInfo.
- # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it.
- if is_windows:
- # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow.
- # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555#
- import ctypes.wintypes
- CSIDL_PERSONAL = 5 # My Documents
- SHGFP_TYPE_CURRENT = 0 # Get current, not default value
-
- buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
- ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
- documentspath = buf.value
- einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
- else:
- einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF]))
-
- # Check if the file exists.
- if os.path.isfile(einfo):
-
- # Open the file and read it, picking out the latest executable's path.
- with open(einfo) as f:
- content = f.read()
- if content:
- try:
- base = re.search(r" = (.*)Versions", content).group(1)
- except AttributeError:
- sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then "
- f"try again.")
- return False
- if os.path.exists(base):
- executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions")
-
- # Finally, check the path for an actual executable.
- # If we find one, great. Set up the SC2PATH.
- if os.path.isfile(executable):
- sc2_logger.info(f"Found an SC2 install at {base}!")
- sc2_logger.debug(f"Latest executable at {executable}.")
- os.environ["SC2PATH"] = base
- sc2_logger.debug(f"SC2PATH set to {base}.")
- return True
- else:
- sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.")
- else:
- sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
- else:
- sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. "
- f"If that fails, please run /set_path with your SC2 install directory.")
- return False
-
-
-def is_mod_installed_correctly() -> bool:
- """Searches for all required files."""
- if "SC2PATH" not in os.environ:
- check_game_install_path()
-
- mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign')
- modfile = os.environ["SC2PATH"] / Path("Mods/Archipelago.SC2Mod")
- wol_required_maps = [
- "ap_thanson01.SC2Map", "ap_thanson02.SC2Map", "ap_thanson03a.SC2Map", "ap_thanson03b.SC2Map",
- "ap_thorner01.SC2Map", "ap_thorner02.SC2Map", "ap_thorner03.SC2Map", "ap_thorner04.SC2Map", "ap_thorner05s.SC2Map",
- "ap_traynor01.SC2Map", "ap_traynor02.SC2Map", "ap_traynor03.SC2Map",
- "ap_ttosh01.SC2Map", "ap_ttosh02.SC2Map", "ap_ttosh03a.SC2Map", "ap_ttosh03b.SC2Map",
- "ap_ttychus01.SC2Map", "ap_ttychus02.SC2Map", "ap_ttychus03.SC2Map", "ap_ttychus04.SC2Map", "ap_ttychus05.SC2Map",
- "ap_tvalerian01.SC2Map", "ap_tvalerian02a.SC2Map", "ap_tvalerian02b.SC2Map", "ap_tvalerian03.SC2Map",
- "ap_tzeratul01.SC2Map", "ap_tzeratul02.SC2Map", "ap_tzeratul03.SC2Map", "ap_tzeratul04.SC2Map"
- ]
- needs_files = False
-
- # Check for maps.
- missing_maps = []
- for mapfile in wol_required_maps:
- if not os.path.isfile(mapdir / mapfile):
- missing_maps.append(mapfile)
- if len(missing_maps) >= 19:
- sc2_logger.warning(f"All map files missing from {mapdir}.")
- needs_files = True
- elif len(missing_maps) > 0:
- for map in missing_maps:
- sc2_logger.debug(f"Missing {map} from {mapdir}.")
- sc2_logger.warning(f"Missing {len(missing_maps)} map files.")
- needs_files = True
- else: # Must be no maps missing
- sc2_logger.info(f"All maps found in {mapdir}.")
-
- # Check for mods.
- if os.path.isfile(modfile):
- sc2_logger.info(f"Archipelago mod found at {modfile}.")
- else:
- sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.")
- needs_files = True
-
- # Final verdict.
- if needs_files:
- sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.")
- return False
- else:
- return True
-
-
-class DllDirectory:
- # Credit to Black Sliver for this code.
- # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw
- _old: typing.Optional[str] = None
- _new: typing.Optional[str] = None
-
- def __init__(self, new: typing.Optional[str]):
- self._new = new
-
- def __enter__(self):
- old = self.get()
- if self.set(self._new):
- self._old = old
-
- def __exit__(self, *args):
- if self._old is not None:
- self.set(self._old)
-
- @staticmethod
- def get() -> typing.Optional[str]:
- if sys.platform == "win32":
- n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
- buf = ctypes.create_unicode_buffer(n)
- ctypes.windll.kernel32.GetDllDirectoryW(n, buf)
- return buf.value
- # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
- return None
-
- @staticmethod
- def set(s: typing.Optional[str]) -> bool:
- if sys.platform == "win32":
- return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0
- # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
- return False
-
-
-def download_latest_release_zip(owner: str, repo: str, current_version: str = None, force_download=False) -> (str, str):
- """Downloads the latest release of a GitHub repo to the current directory as a .zip file."""
- import requests
-
- headers = {"Accept": 'application/vnd.github.v3+json'}
- url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
-
- r1 = requests.get(url, headers=headers)
- if r1.status_code == 200:
- latest_version = r1.json()["tag_name"]
- sc2_logger.info(f"Latest version: {latest_version}.")
- else:
- sc2_logger.warning(f"Status code: {r1.status_code}")
- sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.")
- sc2_logger.warning(f"text: {r1.text}")
- return "", current_version
-
- if (force_download is False) and (current_version == latest_version):
- sc2_logger.info("Latest version already installed.")
- return "", current_version
-
- sc2_logger.info(f"Attempting to download version {latest_version} of {repo}.")
- download_url = r1.json()["assets"][0]["browser_download_url"]
-
- r2 = requests.get(download_url, headers=headers)
- if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
- with open(f"{repo}.zip", "wb") as fh:
- fh.write(r2.content)
- sc2_logger.info(f"Successfully downloaded {repo}.zip.")
- return f"{repo}.zip", latest_version
- else:
- sc2_logger.warning(f"Status code: {r2.status_code}")
- sc2_logger.warning("Download failed.")
- sc2_logger.warning(f"text: {r2.text}")
- return "", current_version
-
-
-def is_mod_update_available(owner: str, repo: str, current_version: str) -> bool:
- import requests
-
- headers = {"Accept": 'application/vnd.github.v3+json'}
- url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
-
- r1 = requests.get(url, headers=headers)
- if r1.status_code == 200:
- latest_version = r1.json()["tag_name"]
- if current_version != latest_version:
- return True
- else:
- return False
-
- else:
- sc2_logger.warning(f"Failed to reach GitHub while checking for updates.")
- sc2_logger.warning(f"Status code: {r1.status_code}")
- sc2_logger.warning(f"text: {r1.text}")
- return False
-
-
-if __name__ == '__main__':
- colorama.init()
- asyncio.run(main())
- colorama.deinit()
+ Utils.init_logging("Starcraft2Client", exception_logger="Client")
+ launch()
diff --git a/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png
new file mode 100644
index 00000000..8fb366b9
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png differ
diff --git a/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png
new file mode 100644
index 00000000..336dc5f7
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png differ
diff --git a/WebHostLib/static/static/icons/sc2/advanceballistics.png b/WebHostLib/static/static/icons/sc2/advanceballistics.png
new file mode 100644
index 00000000..1bf7df9f
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/advanceballistics.png differ
diff --git a/WebHostLib/static/static/icons/sc2/autoturretblackops.png b/WebHostLib/static/static/icons/sc2/autoturretblackops.png
new file mode 100644
index 00000000..55270783
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/autoturretblackops.png differ
diff --git a/WebHostLib/static/static/icons/sc2/biomechanicaldrone.png b/WebHostLib/static/static/icons/sc2/biomechanicaldrone.png
new file mode 100644
index 00000000..e7ebf403
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/biomechanicaldrone.png differ
diff --git a/WebHostLib/static/static/icons/sc2/burstcapacitors.png b/WebHostLib/static/static/icons/sc2/burstcapacitors.png
new file mode 100644
index 00000000..3af9b20a
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/burstcapacitors.png differ
diff --git a/WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png b/WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png
new file mode 100644
index 00000000..d1c0c6c9
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png differ
diff --git a/WebHostLib/static/static/icons/sc2/cyclone.png b/WebHostLib/static/static/icons/sc2/cyclone.png
new file mode 100644
index 00000000..d2016116
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/cyclone.png differ
diff --git a/WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png b/WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png
new file mode 100644
index 00000000..351be570
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png differ
diff --git a/WebHostLib/static/static/icons/sc2/drillingclaws.png b/WebHostLib/static/static/icons/sc2/drillingclaws.png
new file mode 100644
index 00000000..2b067a6e
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/drillingclaws.png differ
diff --git a/WebHostLib/static/static/icons/sc2/emergencythrusters.png b/WebHostLib/static/static/icons/sc2/emergencythrusters.png
new file mode 100644
index 00000000..159fba37
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/emergencythrusters.png differ
diff --git a/WebHostLib/static/static/icons/sc2/hellionbattlemode.png b/WebHostLib/static/static/icons/sc2/hellionbattlemode.png
new file mode 100644
index 00000000..56bfd98c
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/hellionbattlemode.png differ
diff --git a/WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png b/WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png
new file mode 100644
index 00000000..40a5991e
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png differ
diff --git a/WebHostLib/static/static/icons/sc2/hyperflightrotors.png b/WebHostLib/static/static/icons/sc2/hyperflightrotors.png
new file mode 100644
index 00000000..37532584
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/hyperflightrotors.png differ
diff --git a/WebHostLib/static/static/icons/sc2/hyperfluxor.png b/WebHostLib/static/static/icons/sc2/hyperfluxor.png
new file mode 100644
index 00000000..cdd95bb5
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/hyperfluxor.png differ
diff --git a/WebHostLib/static/static/icons/sc2/impalerrounds.png b/WebHostLib/static/static/icons/sc2/impalerrounds.png
new file mode 100644
index 00000000..b00e0c47
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/impalerrounds.png differ
diff --git a/WebHostLib/static/static/icons/sc2/improvedburstlaser.png b/WebHostLib/static/static/icons/sc2/improvedburstlaser.png
new file mode 100644
index 00000000..8a48e38e
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/improvedburstlaser.png differ
diff --git a/WebHostLib/static/static/icons/sc2/improvedsiegemode.png b/WebHostLib/static/static/icons/sc2/improvedsiegemode.png
new file mode 100644
index 00000000..f19dad95
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/improvedsiegemode.png differ
diff --git a/WebHostLib/static/static/icons/sc2/interferencematrix.png b/WebHostLib/static/static/icons/sc2/interferencematrix.png
new file mode 100644
index 00000000..ced928aa
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/interferencematrix.png differ
diff --git a/WebHostLib/static/static/icons/sc2/internalizedtechmodule.png b/WebHostLib/static/static/icons/sc2/internalizedtechmodule.png
new file mode 100644
index 00000000..e97f3db0
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/internalizedtechmodule.png differ
diff --git a/WebHostLib/static/static/icons/sc2/jotunboosters.png b/WebHostLib/static/static/icons/sc2/jotunboosters.png
new file mode 100644
index 00000000..25720306
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/jotunboosters.png differ
diff --git a/WebHostLib/static/static/icons/sc2/jumpjets.png b/WebHostLib/static/static/icons/sc2/jumpjets.png
new file mode 100644
index 00000000..dfdfef40
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/jumpjets.png differ
diff --git a/WebHostLib/static/static/icons/sc2/lasertargetingsystem.png b/WebHostLib/static/static/icons/sc2/lasertargetingsystem.png
new file mode 100644
index 00000000..c57899b2
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/lasertargetingsystem.png differ
diff --git a/WebHostLib/static/static/icons/sc2/liberator.png b/WebHostLib/static/static/icons/sc2/liberator.png
new file mode 100644
index 00000000..31507be5
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/liberator.png differ
diff --git a/WebHostLib/static/static/icons/sc2/lockdown.png b/WebHostLib/static/static/icons/sc2/lockdown.png
new file mode 100644
index 00000000..a2e7f5dc
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/lockdown.png differ
diff --git a/WebHostLib/static/static/icons/sc2/magfieldaccelerator.png b/WebHostLib/static/static/icons/sc2/magfieldaccelerator.png
new file mode 100644
index 00000000..0272b4b7
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/magfieldaccelerator.png differ
diff --git a/WebHostLib/static/static/icons/sc2/magrailmunitions.png b/WebHostLib/static/static/icons/sc2/magrailmunitions.png
new file mode 100644
index 00000000..ec303498
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/magrailmunitions.png differ
diff --git a/WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png b/WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png
new file mode 100644
index 00000000..1c7ce9d6
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png differ
diff --git a/WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png b/WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png
new file mode 100644
index 00000000..04d68d35
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png differ
diff --git a/WebHostLib/static/static/icons/sc2/opticalflare.png b/WebHostLib/static/static/icons/sc2/opticalflare.png
new file mode 100644
index 00000000..f888fd51
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/opticalflare.png differ
diff --git a/WebHostLib/static/static/icons/sc2/optimizedlogistics.png b/WebHostLib/static/static/icons/sc2/optimizedlogistics.png
new file mode 100644
index 00000000..dcf5fd72
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/optimizedlogistics.png differ
diff --git a/WebHostLib/static/static/icons/sc2/reapercombatdrugs.png b/WebHostLib/static/static/icons/sc2/reapercombatdrugs.png
new file mode 100644
index 00000000..b9f2f055
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/reapercombatdrugs.png differ
diff --git a/WebHostLib/static/static/icons/sc2/restoration.png b/WebHostLib/static/static/icons/sc2/restoration.png
new file mode 100644
index 00000000..f5c94e1a
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/restoration.png differ
diff --git a/WebHostLib/static/static/icons/sc2/ripwavemissiles.png b/WebHostLib/static/static/icons/sc2/ripwavemissiles.png
new file mode 100644
index 00000000..f68e8203
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/ripwavemissiles.png differ
diff --git a/WebHostLib/static/static/icons/sc2/shreddermissile.png b/WebHostLib/static/static/icons/sc2/shreddermissile.png
new file mode 100644
index 00000000..40899095
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/shreddermissile.png differ
diff --git a/WebHostLib/static/static/icons/sc2/siegetank-spidermines.png b/WebHostLib/static/static/icons/sc2/siegetank-spidermines.png
new file mode 100644
index 00000000..1b9f8cf0
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/siegetank-spidermines.png differ
diff --git a/WebHostLib/static/static/icons/sc2/siegetankrange.png b/WebHostLib/static/static/icons/sc2/siegetankrange.png
new file mode 100644
index 00000000..5aef00a6
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/siegetankrange.png differ
diff --git a/WebHostLib/static/static/icons/sc2/specialordance.png b/WebHostLib/static/static/icons/sc2/specialordance.png
new file mode 100644
index 00000000..4f7410d7
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/specialordance.png differ
diff --git a/WebHostLib/static/static/icons/sc2/spidermine.png b/WebHostLib/static/static/icons/sc2/spidermine.png
new file mode 100644
index 00000000..bb39cf0b
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/spidermine.png differ
diff --git a/WebHostLib/static/static/icons/sc2/staticempblast.png b/WebHostLib/static/static/icons/sc2/staticempblast.png
new file mode 100644
index 00000000..38f36151
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/staticempblast.png differ
diff --git a/WebHostLib/static/static/icons/sc2/superstimpack.png b/WebHostLib/static/static/icons/sc2/superstimpack.png
new file mode 100644
index 00000000..0fba8ce5
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/superstimpack.png differ
diff --git a/WebHostLib/static/static/icons/sc2/targetingoptics.png b/WebHostLib/static/static/icons/sc2/targetingoptics.png
new file mode 100644
index 00000000..057a40f0
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/targetingoptics.png differ
diff --git a/WebHostLib/static/static/icons/sc2/terran-cloak-color.png b/WebHostLib/static/static/icons/sc2/terran-cloak-color.png
new file mode 100644
index 00000000..44d1bb95
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/terran-cloak-color.png differ
diff --git a/WebHostLib/static/static/icons/sc2/terran-emp-color.png b/WebHostLib/static/static/icons/sc2/terran-emp-color.png
new file mode 100644
index 00000000..972b828c
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/terran-emp-color.png differ
diff --git a/WebHostLib/static/static/icons/sc2/terrandefendermodestructureattack.png b/WebHostLib/static/static/icons/sc2/terrandefendermodestructureattack.png
new file mode 100644
index 00000000..9d598265
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/terrandefendermodestructureattack.png differ
diff --git a/WebHostLib/static/static/icons/sc2/thorsiegemode.png b/WebHostLib/static/static/icons/sc2/thorsiegemode.png
new file mode 100644
index 00000000..a298fb57
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/thorsiegemode.png differ
diff --git a/WebHostLib/static/static/icons/sc2/transformationservos.png b/WebHostLib/static/static/icons/sc2/transformationservos.png
new file mode 100644
index 00000000..f7f0524a
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/transformationservos.png differ
diff --git a/WebHostLib/static/static/icons/sc2/valkyrie.png b/WebHostLib/static/static/icons/sc2/valkyrie.png
new file mode 100644
index 00000000..9cbf339b
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/valkyrie.png differ
diff --git a/WebHostLib/static/static/icons/sc2/warpjump.png b/WebHostLib/static/static/icons/sc2/warpjump.png
new file mode 100644
index 00000000..ff0a7b1a
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/warpjump.png differ
diff --git a/WebHostLib/static/static/icons/sc2/widowmine-attackrange.png b/WebHostLib/static/static/icons/sc2/widowmine-attackrange.png
new file mode 100644
index 00000000..8f5e09c6
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/widowmine-attackrange.png differ
diff --git a/WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png b/WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png
new file mode 100644
index 00000000..7097db05
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png differ
diff --git a/WebHostLib/static/static/icons/sc2/widowmine.png b/WebHostLib/static/static/icons/sc2/widowmine.png
new file mode 100644
index 00000000..802c49a8
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/widowmine.png differ
diff --git a/WebHostLib/static/static/icons/sc2/widowminehidden.png b/WebHostLib/static/static/icons/sc2/widowminehidden.png
new file mode 100644
index 00000000..e568742e
Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/widowminehidden.png differ
diff --git a/WebHostLib/static/styles/sc2wolTracker.css b/WebHostLib/static/styles/sc2wolTracker.css
index b68668ec..a7d8bd28 100644
--- a/WebHostLib/static/styles/sc2wolTracker.css
+++ b/WebHostLib/static/styles/sc2wolTracker.css
@@ -9,7 +9,7 @@
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
- width: 500px;
+ width: 710px;
background-color: #525494;
}
@@ -34,10 +34,12 @@
max-height: 40px;
border: 1px solid #000000;
filter: grayscale(100%) contrast(75%) brightness(20%);
+ background-color: black;
}
#inventory-table img.acquired{
filter: none;
+ background-color: black;
}
#inventory-table div.counted-item {
@@ -52,7 +54,7 @@
}
#location-table{
- width: 500px;
+ width: 710px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
diff --git a/WebHostLib/templates/sc2wolTracker.html b/WebHostLib/templates/sc2wolTracker.html
index af27e30b..49c31a57 100644
--- a/WebHostLib/templates/sc2wolTracker.html
+++ b/WebHostLib/templates/sc2wolTracker.html
@@ -11,7 +11,7 @@
-
+ |
Starting Resources
|
@@ -26,7 +26,7 @@
-->
-
+ |
Weapon & Armor Upgrades
|
@@ -37,120 +37,266 @@
 |
 |
 |
+ |
+  |
+  |
-
+ |
Base
|
-  |
-  |
-  |
-
-
+  |
'] }}) |
'] }}) |
+ '] }}) |
+ '] }}) |
+ |
+  |
'] }}) |
'] }}) |
- |
+
+
+ '] }}) |
+  |
+  |
+ |
'] }}) |
'] }}) |
- '] }}) |
- '] }}) |
+ |
+  |
+  |
+ |
+  |
+ |
+  |
-
+ |  |
+ |
+  |
+ |
+  |
+ |
+  |
+
+
+
Infantry
|
-
-
-  |
-  |
-  |
-  |
-  |
-
-
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
-
-
-
+ | |
+
Vehicles
|
-  |
-  |
-  |
-  |
-  |
-
-
+  |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ |
+  |
'] }}) |
'] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
-
+ |  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ |
+ |
+  |
+
+
+  |
+ '] }}) |
+ '] }}) |
+  |
+ '] }}) |
+ |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ |
+ '] }}) |
+ '] }}) |
+
+
+  |
+ '] }}) |
+ '] }}) |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+
+
+  |
+ '] }}) |
+ '] }}) |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+
+
+ |
+ '] }}) |
+ |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+
+
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ |
+ |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+
+
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ |
+  |
+ '] }}) |
+ '] }}) |
+  |
+
+
+ |
+  |
+ '] }}) |
+
+
+ |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+
+
+ |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+
+
+
Starships
|
-  |
-  |
-  |
-  |
-  |
-
-
+  |
'] }}) |
'] }}) |
+ '] }}) |
+ '] }}) |
+ |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+
+
+  |
'] }}) |
'] }}) |
+ '] }}) |
+ |
+ |
+ '] }}) |
+
+
+  |
'] }}) |
'] }}) |
- '] }}) |
+ '] }}) |
+ '] }}) |
+ |
+  |
+ '] }}) |
+ '] }}) |
+
+
+  |
+  |
'] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ |
+  |
+
+
+  |
'] }}) |
'] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
-
- Dominion
- |
+ |
+ '] }}) |
+ |
+  |
+ '] }}) |
+ '] }}) |
+ '] }}) |
+ '] }}) |
-  |
-  |
-  |
-
-
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
- '] }}) |
-
-
-
+ |
Mercenaries
|
@@ -165,36 +311,18 @@
 |
-
- Lab Upgrades
+ |
+ General Upgrades
|
-  |
-  |
-  |
-  |
-  |
-  |
-  |
-  |
-  |
+ '] }}) |
 |
-
-
-  |
-  |
-  |
-  |
-  |
-  |
 |
-  |
-  |
-  |
+  |
-
+ |
Protoss Units
|
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py
index 5b89495e..4261c27e 100644
--- a/WebHostLib/tracker.py
+++ b/WebHostLib/tracker.py
@@ -990,6 +990,7 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
SC2WOL_LOC_ID_OFFSET = 1000
SC2WOL_ITEM_ID_OFFSET = 1000
+
icons = {
"Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png",
"Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png",
@@ -1034,15 +1035,36 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg",
"Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
+ "Super Stimpack (Marine)": "/static/static/icons/sc2/superstimpack.png",
"Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png",
+ "Laser Targeting System (Marine)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Magrail Munitions (Marine)": "/static/static/icons/sc2/magrailmunitions.png",
+ "Optimized Logistics (Marine)": "/static/static/icons/sc2/optimizedlogistics.png",
"Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png",
"Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png",
+ "Restoration (Medic)": "/static/static/icons/sc2/restoration.png",
+ "Optical Flare (Medic)": "/static/static/icons/sc2/opticalflare.png",
+ "Optimized Logistics (Medic)": "/static/static/icons/sc2/optimizedlogistics.png",
"Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png",
"Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png",
+ "Stimpack (Firebat)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
+ "Super Stimpack (Firebat)": "/static/static/icons/sc2/superstimpack.png",
+ "Optimized Logistics (Firebat)": "/static/static/icons/sc2/optimizedlogistics.png",
"Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png",
"Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png",
+ "Stimpack (Marauder)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
+ "Super Stimpack (Marauder)": "/static/static/icons/sc2/superstimpack.png",
+ "Laser Targeting System (Marauder)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Magrail Munitions (Marauder)": "/static/static/icons/sc2/magrailmunitions.png",
+ "Internal Tech Module (Marauder)": "/static/static/icons/sc2/internalizedtechmodule.png",
"U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png",
"G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png",
+ "Stimpack (Reaper)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
+ "Super Stimpack (Reaper)": "/static/static/icons/sc2/superstimpack.png",
+ "Laser Targeting System (Reaper)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Advanced Cloaking Field (Reaper)": "/static/static/icons/sc2/terran-cloak-color.png",
+ "Spider Mines (Reaper)": "/static/static/icons/sc2/spidermine.png",
+ "Combat Drugs (Reaper)": "/static/static/icons/sc2/reapercombatdrugs.png",
"Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg",
"Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg",
@@ -1052,14 +1074,35 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png",
"Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png",
- "Cerberus Mine (Vulture)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
+ "Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.png",
+ "Smart Servos (Hellion)": "/static/static/icons/sc2/transformationservos.png",
+ "Optimized Logistics (Hellion)": "/static/static/icons/sc2/optimizedlogistics.png",
+ "Jump Jets (Hellion)": "/static/static/icons/sc2/jumpjets.png",
+ "Stimpack (Hellion)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
+ "Super Stimpack (Hellion)": "/static/static/icons/sc2/superstimpack.png",
+ "Cerberus Mine (Spider Mine)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
+ "High Explosive Munition (Spider Mine)": "/static/static/icons/sc2/high-explosive-spidermine.png",
"Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png",
+ "Ion Thrusters (Vulture)": "/static/static/icons/sc2/emergencythrusters.png",
+ "Auto Launchers (Vulture)": "/static/static/icons/sc2/jotunboosters.png",
"Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png",
"Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png",
+ "Jump Jets (Goliath)": "/static/static/icons/sc2/jumpjets.png",
+ "Optimized Logistics (Goliath)": "/static/static/icons/sc2/optimizedlogistics.png",
"Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png",
"Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
+ "Hyperfluxor (Diamondback)": "/static/static/icons/sc2/hyperfluxor.png",
+ "Burst Capacitors (Diamondback)": "/static/static/icons/sc2/burstcapacitors.png",
+ "Optimized Logistics (Diamondback)": "/static/static/icons/sc2/optimizedlogistics.png",
"Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png",
"Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png",
+ "Jump Jets (Siege Tank)": "/static/static/icons/sc2/jumpjets.png",
+ "Spider Mines (Siege Tank)": "/static/static/icons/sc2/siegetank-spidermines.png",
+ "Smart Servos (Siege Tank)": "/static/static/icons/sc2/transformationservos.png",
+ "Graduating Range (Siege Tank)": "/static/static/icons/sc2/siegetankrange.png",
+ "Laser Targeting System (Siege Tank)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Advanced Siege Tech (Siege Tank)": "/static/static/icons/sc2/improvedsiegemode.png",
+ "Internal Tech Module (Siege Tank)": "/static/static/icons/sc2/internalizedtechmodule.png",
"Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg",
"Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg",
@@ -1069,25 +1112,77 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png",
"Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png",
+ "Expanded Hull (Medivac)": "/static/static/icons/sc2/neosteelfortifiedarmor.png",
+ "Afterburners (Medivac)": "/static/static/icons/sc2/medivacemergencythrusters.png",
"Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png",
"Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png",
+ "Advanced Laser Technology (Wraith)": "/static/static/icons/sc2/improvedburstlaser.png",
"Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png",
"Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png",
- "Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
+ "Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.png",
+ "Magrail Munitions (Viking)": "/static/static/icons/sc2/magrailmunitions.png",
+ "Cross-Spectrum Dampeners (Banshee)": "/static/static/icons/sc2/crossspectrumdampeners.png",
+ "Advanced Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
"Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png",
+ "Hyperflight Rotors (Banshee)": "/static/static/icons/sc2/hyperflightrotors.png",
+ "Laser Targeting System (Banshee)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Internal Tech Module (Banshee)": "/static/static/icons/sc2/internalizedtechmodule.png",
"Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png",
"Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
+ "Tactical Jump (Battlecruiser)": "/static/static/icons/sc2/warpjump.png",
+ "Cloak (Battlecruiser)": "/static/static/icons/sc2/terran-cloak-color.png",
+ "ATX Laser Battery (Battlecruiser)": "/static/static/icons/sc2/specialordance.png",
+ "Optimized Logistics (Battlecruiser)": "/static/static/icons/sc2/optimizedlogistics.png",
+ "Internal Tech Module (Battlecruiser)": "/static/static/icons/sc2/internalizedtechmodule.png",
"Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg",
"Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg",
"Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg",
+ "Widow Mine": "/static/static/icons/sc2/widowmine.png",
+ "Cyclone": "/static/static/icons/sc2/cyclone.png",
+ "Liberator": "/static/static/icons/sc2/liberator.png",
+ "Valkyrie": "/static/static/icons/sc2/valkyrie.png",
+
"Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png",
"Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png",
+ "EMP Rounds (Ghost)": "/static/static/icons/sc2/terran-emp-color.png",
+ "Lockdown (Ghost)": "/static/static/icons/sc2/lockdown.png",
"Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png",
"Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png",
+ "Impaler Rounds (Spectre)": "/static/static/icons/sc2/impalerrounds.png",
"330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png",
"Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png",
+ "High Impact Payload (Thor)": "/static/static/icons/sc2/thorsiegemode.png",
+ "Smart Servos (Thor)": "/static/static/icons/sc2/transformationservos.png",
+
+ "Optimized Logistics (Predator)": "/static/static/icons/sc2/optimizedlogistics.png",
+ "Drilling Claws (Widow Mine)": "/static/static/icons/sc2/drillingclaws.png",
+ "Concealment (Widow Mine)": "/static/static/icons/sc2/widowminehidden.png",
+ "Black Market Launchers (Widow Mine)": "/static/static/icons/sc2/widowmine-attackrange.png",
+ "Executioner Missiles (Widow Mine)": "/static/static/icons/sc2/widowmine-deathblossom.png",
+ "Mag-Field Accelerators (Cyclone)": "/static/static/icons/sc2/magfieldaccelerator.png",
+ "Mag-Field Launchers (Cyclone)": "/static/static/icons/sc2/cyclonerangeupgrade.png",
+ "Targeting Optics (Cyclone)": "/static/static/icons/sc2/targetingoptics.png",
+ "Rapid Fire Launchers (Cyclone)": "/static/static/icons/sc2/ripwavemissiles.png",
+ "Bio Mechanical Repair Drone (Raven)": "/static/static/icons/sc2/biomechanicaldrone.png",
+ "Spider Mines (Raven)": "/static/static/icons/sc2/siegetank-spidermines.png",
+ "Railgun Turret (Raven)": "/static/static/icons/sc2/autoturretblackops.png",
+ "Hunter-Seeker Weapon (Raven)": "/static/static/icons/sc2/specialordance.png",
+ "Interference Matrix (Raven)": "/static/static/icons/sc2/interferencematrix.png",
+ "Anti-Armor Missile (Raven)": "/static/static/icons/sc2/shreddermissile.png",
+ "Internal Tech Module (Raven)": "/static/static/icons/sc2/internalizedtechmodule.png",
+ "EMP Shockwave (Science Vessel)": "/static/static/icons/sc2/staticempblast.png",
+ "Defensive Matrix (Science Vessel)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
+ "Advanced Ballistics (Liberator)": "/static/static/icons/sc2/advanceballistics.png",
+ "Raid Artillery (Liberator)": "/static/static/icons/sc2/terrandefendermodestructureattack.png",
+ "Cloak (Liberator)": "/static/static/icons/sc2/terran-cloak-color.png",
+ "Laser Targeting System (Liberator)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Optimized Logistics (Liberator)": "/static/static/icons/sc2/optimizedlogistics.png",
+ "Enhanced Cluster Launchers (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png",
+ "Shaped Hull (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
+ "Burst Lasers (Valkyrie)": "/static/static/icons/sc2/improvedburstlaser.png",
+ "Afterburners (Valkyrie)": "/static/static/icons/sc2/medivacemergencythrusters.png",
"War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg",
"Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg",
@@ -1109,14 +1204,15 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png",
"Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png",
- "Shrike Turret": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
- "Fortified Bunker": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
+ "Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
+ "Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
"Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png",
"Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png",
"Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png",
"Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png",
"Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png",
- "Regenerative Bio-Steel": "https://static.wikia.nocookie.net/starcraft/images/d/d3/SC2_Lab_BioSteel_Icon.png",
+ "Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png",
+ "Regenerative Bio-Steel Level 2": "/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png",
"Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png",
"Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png",
@@ -1132,40 +1228,71 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Nothing": "",
}
-
sc2wol_location_ids = {
- "Liberation Day": [SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 101, SC2WOL_LOC_ID_OFFSET + 102, SC2WOL_LOC_ID_OFFSET + 103, SC2WOL_LOC_ID_OFFSET + 104, SC2WOL_LOC_ID_OFFSET + 105, SC2WOL_LOC_ID_OFFSET + 106],
- "The Outlaws": [SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 201],
- "Zero Hour": [SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 301, SC2WOL_LOC_ID_OFFSET + 302, SC2WOL_LOC_ID_OFFSET + 303],
- "Evacuation": [SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 401, SC2WOL_LOC_ID_OFFSET + 402, SC2WOL_LOC_ID_OFFSET + 403],
- "Outbreak": [SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 501, SC2WOL_LOC_ID_OFFSET + 502],
- "Safe Haven": [SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 601, SC2WOL_LOC_ID_OFFSET + 602, SC2WOL_LOC_ID_OFFSET + 603],
- "Haven's Fall": [SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 701, SC2WOL_LOC_ID_OFFSET + 702, SC2WOL_LOC_ID_OFFSET + 703],
- "Smash and Grab": [SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 801, SC2WOL_LOC_ID_OFFSET + 802, SC2WOL_LOC_ID_OFFSET + 803, SC2WOL_LOC_ID_OFFSET + 804],
- "The Dig": [SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 901, SC2WOL_LOC_ID_OFFSET + 902, SC2WOL_LOC_ID_OFFSET + 903],
- "The Moebius Factor": [SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1003, SC2WOL_LOC_ID_OFFSET + 1004, SC2WOL_LOC_ID_OFFSET + 1005, SC2WOL_LOC_ID_OFFSET + 1006, SC2WOL_LOC_ID_OFFSET + 1007, SC2WOL_LOC_ID_OFFSET + 1008],
- "Supernova": [SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1101, SC2WOL_LOC_ID_OFFSET + 1102, SC2WOL_LOC_ID_OFFSET + 1103, SC2WOL_LOC_ID_OFFSET + 1104],
- "Maw of the Void": [SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1201, SC2WOL_LOC_ID_OFFSET + 1202, SC2WOL_LOC_ID_OFFSET + 1203, SC2WOL_LOC_ID_OFFSET + 1204, SC2WOL_LOC_ID_OFFSET + 1205],
- "Devil's Playground": [SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1301, SC2WOL_LOC_ID_OFFSET + 1302],
- "Welcome to the Jungle": [SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1401, SC2WOL_LOC_ID_OFFSET + 1402, SC2WOL_LOC_ID_OFFSET + 1403],
- "Breakout": [SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1501, SC2WOL_LOC_ID_OFFSET + 1502],
- "Ghost of a Chance": [SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1601, SC2WOL_LOC_ID_OFFSET + 1602, SC2WOL_LOC_ID_OFFSET + 1603, SC2WOL_LOC_ID_OFFSET + 1604, SC2WOL_LOC_ID_OFFSET + 1605],
- "The Great Train Robbery": [SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1701, SC2WOL_LOC_ID_OFFSET + 1702, SC2WOL_LOC_ID_OFFSET + 1703],
- "Cutthroat": [SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1801, SC2WOL_LOC_ID_OFFSET + 1802, SC2WOL_LOC_ID_OFFSET + 1803, SC2WOL_LOC_ID_OFFSET + 1804],
- "Engine of Destruction": [SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 1901, SC2WOL_LOC_ID_OFFSET + 1902, SC2WOL_LOC_ID_OFFSET + 1903, SC2WOL_LOC_ID_OFFSET + 1904, SC2WOL_LOC_ID_OFFSET + 1905],
- "Media Blitz": [SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2001, SC2WOL_LOC_ID_OFFSET + 2002, SC2WOL_LOC_ID_OFFSET + 2003, SC2WOL_LOC_ID_OFFSET + 2004],
- "Piercing the Shroud": [SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2101, SC2WOL_LOC_ID_OFFSET + 2102, SC2WOL_LOC_ID_OFFSET + 2103, SC2WOL_LOC_ID_OFFSET + 2104, SC2WOL_LOC_ID_OFFSET + 2105],
- "Whispers of Doom": [SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2201, SC2WOL_LOC_ID_OFFSET + 2202, SC2WOL_LOC_ID_OFFSET + 2203],
- "A Sinister Turn": [SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2301, SC2WOL_LOC_ID_OFFSET + 2302, SC2WOL_LOC_ID_OFFSET + 2303],
- "Echoes of the Future": [SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2401, SC2WOL_LOC_ID_OFFSET + 2402],
- "In Utter Darkness": [SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2501, SC2WOL_LOC_ID_OFFSET + 2502],
- "Gates of Hell": [SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2601],
- "Belly of the Beast": [SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2701, SC2WOL_LOC_ID_OFFSET + 2702, SC2WOL_LOC_ID_OFFSET + 2703],
- "Shatter the Sky": [SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2801, SC2WOL_LOC_ID_OFFSET + 2802, SC2WOL_LOC_ID_OFFSET + 2803, SC2WOL_LOC_ID_OFFSET + 2804, SC2WOL_LOC_ID_OFFSET + 2805],
+ "Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200),
+ "The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300),
+ "Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400),
+ "Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500),
+ "Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600),
+ "Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700),
+ "Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800),
+ "Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900),
+ "The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000),
+ "The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100),
+ "Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200),
+ "Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300),
+ "Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400),
+ "Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500),
+ "Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600),
+ "Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700),
+ "The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800),
+ "Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900),
+ "Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000),
+ "Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100),
+ "Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200),
+ "Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300),
+ "A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400),
+ "Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500),
+ "In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600),
+ "Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700),
+ "Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800),
+ "Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900),
}
display_data = {}
+ # Grouped Items
+ grouped_item_ids = {
+ "Progressive Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET
+ }
+ grouped_item_replacements = {
+ "Progressive Weapon Upgrade": ["Progressive Infantry Weapon", "Progressive Vehicle Weapon", "Progressive Ship Weapon"],
+ "Progressive Armor Upgrade": ["Progressive Infantry Armor", "Progressive Vehicle Armor", "Progressive Ship Armor"],
+ "Progressive Infantry Upgrade": ["Progressive Infantry Weapon", "Progressive Infantry Armor"],
+ "Progressive Vehicle Upgrade": ["Progressive Vehicle Weapon", "Progressive Vehicle Armor"],
+ "Progressive Ship Upgrade": ["Progressive Ship Weapon", "Progressive Ship Armor"]
+ }
+ grouped_item_replacements["Progressive Weapon/Armor Upgrade"] = grouped_item_replacements["Progressive Weapon Upgrade"] + grouped_item_replacements["Progressive Armor Upgrade"]
+ replacement_item_ids = {
+ "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET,
+ }
+ for grouped_item_name, grouped_item_id in grouped_item_ids.items():
+ count: int = inventory[grouped_item_id]
+ if count > 0:
+ for replacement_item in grouped_item_replacements[grouped_item_name]:
+ replacement_id: int = replacement_item_ids[replacement_item]
+ inventory[replacement_id] = count
+
# Determine display for progressive items
progressive_items = {
"Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
@@ -1173,7 +1300,15 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET
+ "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stimpack (Marine)": 208 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stimpack (Firebat)": 226 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stimpack (Marauder)": 228 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stimpack (Reaper)": 250 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stimpack (Hellion)": 259 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET
}
progressive_names = {
"Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"],
@@ -1181,14 +1316,27 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"],
"Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", "Vehicle Armor Level 2", "Vehicle Armor Level 3"],
"Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", "Ship Weapons Level 2", "Ship Weapons Level 3"],
- "Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"]
+ "Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"],
+ "Progressive Stimpack (Marine)": ["Stimpack (Marine)", "Stimpack (Marine)", "Super Stimpack (Marine)"],
+ "Progressive Stimpack (Firebat)": ["Stimpack (Firebat)", "Stimpack (Firebat)", "Super Stimpack (Firebat)"],
+ "Progressive Stimpack (Marauder)": ["Stimpack (Marauder)", "Stimpack (Marauder)", "Super Stimpack (Marauder)"],
+ "Progressive Stimpack (Reaper)": ["Stimpack (Reaper)", "Stimpack (Reaper)", "Super Stimpack (Reaper)"],
+ "Progressive Stimpack (Hellion)": ["Stimpack (Hellion)", "Stimpack (Hellion)", "Super Stimpack (Hellion)"],
+ "Progressive High Impact Payload (Thor)": ["High Impact Payload (Thor)", "High Impact Payload (Thor)", "Smart Servos (Thor)"],
+ "Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)", "Cross-Spectrum Dampeners (Banshee)", "Advanced Cross-Spectrum Dampeners (Banshee)"],
+ "Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 2"]
}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
display_name = progressive_names[item_name][level]
- base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
+ base_name = (item_name.split(maxsplit=1)[1].lower()
+ .replace(' ', '_')
+ .replace("-", "")
+ .replace("(", "")
+ .replace(")", ""))
display_data[base_name + "_level"] = level
display_data[base_name + "_url"] = icons[display_name]
+ display_data[base_name + "_name"] = display_name
# Multi-items
multi_items = {
@@ -1220,12 +1368,12 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
checks_in_area['Total'] = sum(checks_in_area.values())
return render_template("sc2wolTracker.html",
- inventory=inventory, icons=icons,
- acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
- id in lookup_any_item_id_to_name},
- player=player, team=team, room=room, player_name=playerName,
- checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
- **display_data)
+ inventory=inventory, icons=icons,
+ acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
+ id in lookup_any_item_id_to_name},
+ player=player, team=team, room=room, player_name=playerName,
+ checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
+ **display_data)
def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
diff --git a/setup.py b/setup.py
index 212bcc5d..ce35c0f1 100644
--- a/setup.py
+++ b/setup.py
@@ -80,7 +80,6 @@ non_apworlds: set = {
"Raft",
"Secret of Evermore",
"Slay the Spire",
- "Starcraft 2 Wings of Liberty",
"Sudoku",
"Super Mario 64",
"VVVVVV",
@@ -91,6 +90,7 @@ non_apworlds: set = {
# LogicMixin is broken before 3.10 import revamp
if sys.version_info < (3,10):
non_apworlds.add("Hollow Knight")
+ non_apworlds.add("Starcraft 2 Wings of Liberty")
def download_SNI():
print("Updating SNI")
diff --git a/worlds/_sc2common/bot/maps.py b/worlds/_sc2common/bot/maps.py
index f14b5af9..29ce9f65 100644
--- a/worlds/_sc2common/bot/maps.py
+++ b/worlds/_sc2common/bot/maps.py
@@ -8,18 +8,31 @@ from .paths import Paths
def get(name: str) -> Map:
- # Iterate through 2 folder depths
for map_dir in (p for p in Paths.MAPS.iterdir()):
- if map_dir.is_dir():
- for map_file in (p for p in map_dir.iterdir()):
- if Map.matches_target_map_name(map_file, name):
- return Map(map_file)
- elif Map.matches_target_map_name(map_dir, name):
- return Map(map_dir)
+ map = find_map_in_dir(name, map_dir)
+ if map is not None:
+ return map
raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".")
+# Go deeper
+def find_map_in_dir(name, path):
+ if Map.matches_target_map_name(path, name):
+ return Map(path)
+
+ if path.name.endswith("SC2Map"):
+ return None
+
+ if path.is_dir():
+ for childPath in (p for p in path.iterdir()):
+ map = find_map_in_dir(name, childPath)
+ if map is not None:
+ return map
+
+ return None
+
+
class Map:
def __init__(self, path: Path):
diff --git a/worlds/sc2wol/Client.py b/worlds/sc2wol/Client.py
new file mode 100644
index 00000000..c544cf0c
--- /dev/null
+++ b/worlds/sc2wol/Client.py
@@ -0,0 +1,1201 @@
+from __future__ import annotations
+
+import asyncio
+import copy
+import ctypes
+import logging
+import multiprocessing
+import os.path
+import re
+import sys
+import typing
+import queue
+import zipfile
+import io
+import random
+from pathlib import Path
+
+# CommonClient import first to trigger ModuleUpdater
+from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
+from Utils import init_logging, is_windows
+
+if __name__ == "__main__":
+ init_logging("SC2Client", exception_logger="Client")
+
+logger = logging.getLogger("Client")
+sc2_logger = logging.getLogger("Starcraft2")
+
+import nest_asyncio
+from worlds._sc2common import bot
+from worlds._sc2common.bot.data import Race
+from worlds._sc2common.bot.main import run_game
+from worlds._sc2common.bot.player import Bot
+from worlds.sc2wol import SC2WoLWorld
+from worlds.sc2wol.Items import lookup_id_to_name, get_full_item_list, ItemData, type_flaggroups, upgrade_numbers
+from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
+from worlds.sc2wol.MissionTables import lookup_id_to_mission
+from worlds.sc2wol.Regions import MissionInfo
+
+import colorama
+from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser, JSONtoTextParser, JSONMessagePart
+from MultiServer import mark_raw
+
+loop = asyncio.get_event_loop_policy().new_event_loop()
+nest_asyncio.apply(loop)
+max_bonus: int = 13
+victory_modulo: int = 100
+
+# GitHub repo where the Map/mod data is hosted for /download_data command
+DATA_REPO_OWNER = "Ziktofel"
+DATA_REPO_NAME = "Archipelago-SC2-data"
+DATA_API_VERSION = "API2"
+
+
+# Data version file path.
+# This file is used to tell if the downloaded data are outdated
+# Associated with /download_data command
+def get_metadata_file():
+ return os.environ["SC2PATH"] + os.sep + "ArchipelagoSC2Metadata.txt"
+
+
+class StarcraftClientProcessor(ClientCommandProcessor):
+ ctx: SC2Context
+
+ def _cmd_difficulty(self, difficulty: str = "") -> bool:
+ """Overrides the current difficulty set for the world. Takes the argument casual, normal, hard, or brutal"""
+ options = difficulty.split()
+ num_options = len(options)
+
+ if num_options > 0:
+ difficulty_choice = options[0].lower()
+ if difficulty_choice == "casual":
+ self.ctx.difficulty_override = 0
+ elif difficulty_choice == "normal":
+ self.ctx.difficulty_override = 1
+ elif difficulty_choice == "hard":
+ self.ctx.difficulty_override = 2
+ elif difficulty_choice == "brutal":
+ self.ctx.difficulty_override = 3
+ else:
+ self.output("Unable to parse difficulty '" + options[0] + "'")
+ return False
+
+ self.output("Difficulty set to " + options[0])
+ return True
+
+ else:
+ if self.ctx.difficulty == -1:
+ self.output("Please connect to a seed before checking difficulty.")
+ else:
+ current_difficulty = self.ctx.difficulty
+ if self.ctx.difficulty_override >= 0:
+ current_difficulty = self.ctx.difficulty_override
+ self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][current_difficulty])
+ self.output("To change the difficulty, add the name of the difficulty after the command.")
+ return False
+
+
+ def _cmd_game_speed(self, game_speed: str = "") -> bool:
+ """Overrides the current game speed for the world.
+ Takes the arguments default, slower, slow, normal, fast, faster"""
+ options = game_speed.split()
+ num_options = len(options)
+
+ if num_options > 0:
+ speed_choice = options[0].lower()
+ if speed_choice == "default":
+ self.ctx.game_speed_override = 0
+ elif speed_choice == "slower":
+ self.ctx.game_speed_override = 1
+ elif speed_choice == "slow":
+ self.ctx.game_speed_override = 2
+ elif speed_choice == "normal":
+ self.ctx.game_speed_override = 3
+ elif speed_choice == "fast":
+ self.ctx.game_speed_override = 4
+ elif speed_choice == "faster":
+ self.ctx.game_speed_override = 5
+ else:
+ self.output("Unable to parse game speed '" + options[0] + "'")
+ return False
+
+ self.output("Game speed set to " + options[0])
+ return True
+
+ else:
+ if self.ctx.game_speed == -1:
+ self.output("Please connect to a seed before checking game speed.")
+ else:
+ current_speed = self.ctx.game_speed
+ if self.ctx.game_speed_override >= 0:
+ current_speed = self.ctx.game_speed_override
+ self.output("Current game speed: "
+ + ["Default", "Slower", "Slow", "Normal", "Fast", "Faster"][current_speed])
+ self.output("To change the game speed, add the name of the speed after the command,"
+ " or Default to select based on difficulty.")
+ return False
+
+ def _cmd_color(self, color: str = "") -> bool:
+ player_colors = [
+ "White", "Red", "Blue", "Teal",
+ "Purple", "Yellow", "Orange", "Green",
+ "LightPink", "Violet", "LightGrey", "DarkGreen",
+ "Brown", "LightGreen", "DarkGrey", "Pink",
+ "Rainbow", "Random", "Default"
+ ]
+ match_colors = [player_color.lower() for player_color in player_colors]
+ if color:
+ if color.lower() not in match_colors:
+ self.output(color + " is not a valid color. Available colors: " + ', '.join(player_colors))
+ return False
+ if color.lower() == "random":
+ color = random.choice(player_colors[:16])
+ self.ctx.player_color = match_colors.index(color.lower())
+ self.output("Color set to " + player_colors[self.ctx.player_color])
+ else:
+ self.output("Current player color: " + player_colors[self.ctx.player_color])
+ self.output("To change your colors, add the name of the color after the command.")
+ self.output("Available colors: " + ', '.join(player_colors))
+
+ def _cmd_disable_mission_check(self) -> bool:
+ """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
+ the next mission in a chain the other player is doing."""
+ self.ctx.missions_unlocked = True
+ sc2_logger.info("Mission check has been disabled")
+ return True
+
+ 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])
+
+ self.ctx.play_mission(mission_number)
+
+ else:
+ sc2_logger.info(
+ "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
+ return False
+
+ return True
+
+ def _cmd_available(self) -> bool:
+ """Get what missions are currently available to play"""
+
+ request_available_missions(self.ctx)
+ 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)
+ return True
+
+ @mark_raw
+ def _cmd_set_path(self, path: str = '') -> bool:
+ """Manually set the SC2 install directory (if the automatic detection fails)."""
+ if path:
+ os.environ["SC2PATH"] = path
+ is_mod_installed_correctly()
+ return True
+ else:
+ sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
+ return False
+
+ def _cmd_download_data(self) -> bool:
+ """Download the most recent release of the necessary files for playing SC2 with
+ Archipelago. Will overwrite existing files."""
+ if "SC2PATH" not in os.environ:
+ check_game_install_path()
+
+ if os.path.exists(get_metadata_file()):
+ with open(get_metadata_file(), "r") as f:
+ metadata = f.read()
+ else:
+ metadata = None
+
+ tempzip, metadata = download_latest_release_zip(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION,
+ metadata=metadata, force_download=True)
+
+ if tempzip != '':
+ try:
+ zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
+ sc2_logger.info(f"Download complete. Package installed.")
+ with open(get_metadata_file(), "w") as f:
+ f.write(metadata)
+ finally:
+ os.remove(tempzip)
+ else:
+ sc2_logger.warning("Download aborted/failed. Read the log for more information.")
+ return False
+ return True
+
+
+class SC2JSONtoTextParser(JSONtoTextParser):
+ def __init__(self, ctx):
+ self.handlers = {
+ "ItemSend": self._handle_color,
+ "ItemCheat": self._handle_color,
+ "Hint": self._handle_color,
+ }
+ super().__init__(ctx)
+
+ def _handle_color(self, node: JSONMessagePart):
+ codes = node["color"].split(";")
+ buffer = "".join(self.color_code(code) for code in codes if code in self.color_codes)
+ return buffer + self._handle_text(node) + ''
+
+ def color_code(self, code: str):
+ return ''
+
+
+class SC2Context(CommonContext):
+ command_processor = StarcraftClientProcessor
+ game = "Starcraft 2 Wings of Liberty"
+ items_handling = 0b111
+ difficulty = -1
+ game_speed = -1
+ all_in_choice = 0
+ mission_order = 0
+ player_color = 2
+ mission_req_table: typing.Dict[str, MissionInfo] = {}
+ final_mission: int = 29
+ announcements = queue.Queue()
+ sc2_run_task: typing.Optional[asyncio.Task] = None
+ missions_unlocked: bool = False # allow launching missions ignoring requirements
+ generic_upgrade_missions = 0
+ generic_upgrade_research = 0
+ generic_upgrade_items = 0
+ current_tooltip = None
+ last_loc_list = None
+ difficulty_override = -1
+ game_speed_override = -1
+ mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
+ last_bot: typing.Optional[ArchipelagoBot] = None
+
+ def __init__(self, *args, **kwargs):
+ super(SC2Context, self).__init__(*args, **kwargs)
+ self.raw_text_parser = SC2JSONtoTextParser(self)
+
+ async def server_auth(self, password_requested: bool = False):
+ if password_requested and not self.password:
+ await super(SC2Context, self).server_auth(password_requested)
+ await self.get_username()
+ await self.send_connect()
+
+ def on_package(self, cmd: str, args: dict):
+ if cmd in {"Connected"}:
+ self.difficulty = args["slot_data"]["game_difficulty"]
+ if "game_speed" in args["slot_data"]:
+ self.game_speed = args["slot_data"]["game_speed"]
+ else:
+ self.game_speed = 0
+ self.all_in_choice = args["slot_data"]["all_in_map"]
+ slot_req_table = args["slot_data"]["mission_req"]
+ # Maintaining backwards compatibility with older slot data
+ self.mission_req_table = {
+ mission: MissionInfo(
+ **{field: value for field, value in mission_info.items() if field in MissionInfo._fields}
+ )
+ for mission, mission_info in slot_req_table.items()
+ }
+ self.mission_order = args["slot_data"].get("mission_order", 0)
+ self.final_mission = args["slot_data"].get("final_mission", 29)
+ self.player_color = args["slot_data"].get("player_color", 2)
+ self.generic_upgrade_missions = args["slot_data"].get("generic_upgrade_missions", 0)
+ self.generic_upgrade_items = args["slot_data"].get("generic_upgrade_items", 0)
+ self.generic_upgrade_research = args["slot_data"].get("generic_upgrade_research", 0)
+
+ self.build_location_to_mission_mapping()
+
+ # Looks for the required maps and mods for SC2. Runs check_game_install_path.
+ maps_present = is_mod_installed_correctly()
+ if os.path.exists(get_metadata_file()):
+ with open(get_metadata_file(), "r") as f:
+ current_ver = f.read()
+ sc2_logger.debug(f"Current version: {current_ver}")
+ if is_mod_update_available(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, current_ver):
+ sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
+ elif maps_present:
+ sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). "
+ "Run /download_data to update them.")
+
+
+ def on_print_json(self, args: dict):
+ # goes to this world
+ if "receiving" in args and self.slot_concerns_self(args["receiving"]):
+ relevant = True
+ # found in this world
+ elif "item" in args and self.slot_concerns_self(args["item"].player):
+ relevant = True
+ # not related
+ else:
+ relevant = False
+
+ if relevant:
+ self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"])))
+
+ super(SC2Context, self).on_print_json(args)
+
+ def run_gui(self):
+ from kvui import GameManager, HoverBehavior, ServerToolTip
+ from kivy.app import App
+ from kivy.clock import Clock
+ from kivy.uix.tabbedpanel import TabbedPanelItem
+ from kivy.uix.gridlayout import GridLayout
+ from kivy.lang import Builder
+ from kivy.uix.label import Label
+ from kivy.uix.button import Button
+ from kivy.uix.floatlayout import FloatLayout
+ from kivy.properties import StringProperty
+
+ class HoverableButton(HoverBehavior, Button):
+ pass
+
+ class MissionButton(HoverableButton):
+ tooltip_text = StringProperty("Test")
+ ctx: SC2Context
+
+ def __init__(self, *args, **kwargs):
+ super(HoverableButton, self).__init__(*args, **kwargs)
+ self.layout = FloatLayout()
+ self.popuplabel = ServerToolTip(text=self.text)
+ self.layout.add_widget(self.popuplabel)
+
+ def on_enter(self):
+ self.popuplabel.text = self.tooltip_text
+
+ if self.ctx.current_tooltip:
+ App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
+
+ if self.tooltip_text == "":
+ self.ctx.current_tooltip = None
+ else:
+ App.get_running_app().root.add_widget(self.layout)
+ self.ctx.current_tooltip = self.layout
+
+ def on_leave(self):
+ self.ctx.ui.clear_tooltip()
+
+ @property
+ def ctx(self) -> CommonContext:
+ return App.get_running_app().ctx
+
+ class MissionLayout(GridLayout):
+ pass
+
+ class MissionCategory(GridLayout):
+ pass
+
+ class SC2Manager(GameManager):
+ logging_pairs = [
+ ("Client", "Archipelago"),
+ ("Starcraft2", "Starcraft2"),
+ ]
+ base_title = "Archipelago Starcraft 2 Client"
+
+ mission_panel = None
+ last_checked_locations = {}
+ mission_id_to_button = {}
+ launching: typing.Union[bool, int] = False # if int -> mission ID
+ refresh_from_launching = True
+ first_check = True
+ ctx: SC2Context
+
+ def __init__(self, ctx):
+ super().__init__(ctx)
+
+ def clear_tooltip(self):
+ if self.ctx.current_tooltip:
+ App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
+
+ self.ctx.current_tooltip = None
+
+ def build(self):
+ container = super().build()
+
+ panel = TabbedPanelItem(text="Starcraft 2 Launcher")
+ self.mission_panel = panel.content = MissionLayout()
+
+ self.tabs.add_widget(panel)
+
+ Clock.schedule_interval(self.build_mission_table, 0.5)
+
+ return container
+
+ def build_mission_table(self, dt):
+ if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
+ not self.refresh_from_launching)) or self.first_check:
+ self.refresh_from_launching = True
+
+ self.mission_panel.clear_widgets()
+ if self.ctx.mission_req_table:
+ self.last_checked_locations = self.ctx.checked_locations.copy()
+ self.first_check = False
+
+ self.mission_id_to_button = {}
+ categories = {}
+ available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
+
+ # separate missions into categories
+ for mission in self.ctx.mission_req_table:
+ if not self.ctx.mission_req_table[mission].category in categories:
+ categories[self.ctx.mission_req_table[mission].category] = []
+
+ categories[self.ctx.mission_req_table[mission].category].append(mission)
+
+ for category in categories:
+ category_panel = MissionCategory()
+ if category.startswith('_'):
+ category_display_name = ''
+ else:
+ category_display_name = category
+ category_panel.add_widget(
+ Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1))
+
+ for mission in categories[category]:
+ text: str = mission
+ tooltip: str = ""
+ mission_id: int = self.ctx.mission_req_table[mission].id
+ # Map has uncollected locations
+ if mission in unfinished_missions:
+ text = f"[color=6495ED]{text}[/color]"
+ elif mission in available_missions:
+ text = f"[color=FFFFFF]{text}[/color]"
+ # Map requirements not met
+ else:
+ text = f"[color=a9a9a9]{text}[/color]"
+ tooltip = f"Requires: "
+ if self.ctx.mission_req_table[mission].required_world:
+ tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
+ req_mission in
+ self.ctx.mission_req_table[mission].required_world)
+
+ if self.ctx.mission_req_table[mission].number:
+ tooltip += " and "
+ if self.ctx.mission_req_table[mission].number:
+ tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed"
+ remaining_location_names: typing.List[str] = [
+ self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
+ if loc in self.ctx.missing_locations]
+
+ if mission_id == self.ctx.final_mission:
+ if mission in available_missions:
+ text = f"[color=FFBC95]{mission}[/color]"
+ else:
+ text = f"[color=D0C0BE]{mission}[/color]"
+ if tooltip:
+ tooltip += "\n"
+ tooltip += "Final Mission"
+
+ if remaining_location_names:
+ if tooltip:
+ tooltip += "\n"
+ tooltip += f"Uncollected locations:\n"
+ tooltip += "\n".join(remaining_location_names)
+
+ mission_button = MissionButton(text=text, size_hint_y=None, height=50)
+ mission_button.tooltip_text = tooltip
+ mission_button.bind(on_press=self.mission_callback)
+ self.mission_id_to_button[mission_id] = mission_button
+ category_panel.add_widget(mission_button)
+
+ category_panel.add_widget(Label(text=""))
+ self.mission_panel.add_widget(category_panel)
+
+ elif self.launching:
+ self.refresh_from_launching = False
+
+ self.mission_panel.clear_widgets()
+ self.mission_panel.add_widget(Label(text="Launching Mission: " +
+ lookup_id_to_mission[self.launching]))
+ if self.ctx.ui:
+ self.ctx.ui.clear_tooltip()
+
+ def mission_callback(self, button):
+ if not self.launching:
+ mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button)
+ if self.ctx.play_mission(mission_id):
+ self.launching = mission_id
+ Clock.schedule_once(self.finish_launching, 10)
+
+ def finish_launching(self, dt):
+ self.launching = False
+
+ self.ui = SC2Manager(self)
+ self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
+ import pkgutil
+ data = pkgutil.get_data(SC2WoLWorld.__module__, "Starcraft2.kv").decode()
+ Builder.load_string(data)
+
+ async def shutdown(self):
+ await super(SC2Context, self).shutdown()
+ if self.last_bot:
+ self.last_bot.want_close = True
+ if self.sc2_run_task:
+ self.sc2_run_task.cancel()
+
+ def play_mission(self, mission_id: int) -> bool:
+ if self.missions_unlocked or \
+ is_mission_available(self, mission_id):
+ if self.sc2_run_task:
+ if not self.sc2_run_task.done():
+ sc2_logger.warning("Starcraft 2 Client is still running!")
+ self.sc2_run_task.cancel() # doesn't actually close the game, just stops the python task
+ if self.slot is None:
+ sc2_logger.warning("Launching Mission without Archipelago authentication, "
+ "checks will not be registered to server.")
+ self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id),
+ name="Starcraft 2 Launch")
+ return True
+ else:
+ sc2_logger.info(
+ f"{lookup_id_to_mission[mission_id]} is not currently unlocked. "
+ f"Use /unfinished or /available to see what is available.")
+ return False
+
+ def build_location_to_mission_mapping(self):
+ mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
+ mission_info.id: set() for mission_info in self.mission_req_table.values()
+ }
+
+ for loc in self.server_locations:
+ mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo)
+ mission_id_to_location_ids[mission_id].add(objective)
+ self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in
+ mission_id_to_location_ids.items()}
+
+ def locations_for_mission(self, mission: str):
+ mission_id: int = self.mission_req_table[mission].id
+ objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id]
+ for objective in objectives:
+ yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective
+
+
+async def main():
+ multiprocessing.freeze_support()
+ parser = get_base_parser()
+ parser.add_argument('--name', default=None, help="Slot Name to connect as.")
+ args = parser.parse_args()
+
+ ctx = SC2Context(args.connect, args.password)
+ ctx.auth = args.name
+ if ctx.server_task is None:
+ ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
+
+ if gui_enabled:
+ ctx.run_gui()
+ ctx.run_cli()
+
+ await ctx.exit_event.wait()
+
+ await ctx.shutdown()
+
+
+maps_table = [
+ "ap_liberation_day", "ap_the_outlaws", "ap_zero_hour",
+ "ap_evacuation", "ap_outbreak", "ap_safe_haven", "ap_havens_fall",
+ "ap_smash_and_grab", "ap_the_dig", "ap_the_moebius_factor", "ap_supernova", "ap_maw_of_the_void",
+ "ap_devils_playground", "ap_welcome_to_the_jungle", "ap_breakout", "ap_ghost_of_a_chance",
+ "ap_the_great_train_robbery", "ap_cutthroat", "ap_engine_of_destruction", "ap_media_blitz", "ap_piercing_the_shroud",
+ "ap_whispers_of_doom", "ap_a_sinister_turn", "ap_echoes_of_the_future", "ap_in_utter_darkness",
+ "ap_gates_of_hell", "ap_belly_of_the_beast", "ap_shatter_the_sky", "ap_all_in"
+]
+
+wol_default_categories = [
+ "Mar Sara", "Mar Sara", "Mar Sara", "Colonist", "Colonist", "Colonist", "Colonist",
+ "Artifact", "Artifact", "Artifact", "Artifact", "Artifact", "Covert", "Covert", "Covert", "Covert",
+ "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy",
+ "Char", "Char", "Char", "Char"
+]
+wol_default_category_names = [
+ "Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char"
+]
+
+
+def calculate_items(ctx: SC2Context) -> typing.List[int]:
+ items = ctx.items_received
+ network_item: NetworkItem
+ accumulators: typing.List[int] = [0 for _ in type_flaggroups]
+
+ for network_item in items:
+ name: str = lookup_id_to_name[network_item.item]
+ item_data: ItemData = get_full_item_list()[name]
+
+ # exists exactly once
+ if item_data.quantity == 1:
+ accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number
+
+ # exists multiple times
+ elif item_data.type == "Upgrade" or item_data.type == "Progressive Upgrade":
+ flaggroup = type_flaggroups[item_data.type]
+
+ # Generic upgrades apply only to Weapon / Armor upgrades
+ if item_data.type != "Upgrade" or ctx.generic_upgrade_items == 0:
+ accumulators[flaggroup] += 1 << item_data.number
+ else:
+ for bundled_number in upgrade_numbers[item_data.number]:
+ accumulators[flaggroup] += 1 << bundled_number
+
+ # sum
+ else:
+ accumulators[type_flaggroups[item_data.type]] += item_data.number
+
+ # Upgrades from completed missions
+ if ctx.generic_upgrade_missions > 0:
+ upgrade_flaggroup = type_flaggroups["Upgrade"]
+ num_missions = ctx.generic_upgrade_missions * len(ctx.mission_req_table)
+ amounts = [
+ num_missions // 100,
+ 2 * num_missions // 100,
+ 3 * num_missions // 100
+ ]
+ upgrade_count = 0
+ completed = len([id for id in ctx.mission_id_to_location_ids if SC2WOL_LOC_ID_OFFSET + victory_modulo * id in ctx.checked_locations])
+ for amount in amounts:
+ if completed >= amount:
+ upgrade_count += 1
+ # Equivalent to "Progressive Weapon/Armor Upgrade" item
+ for bundled_number in upgrade_numbers[5]:
+ accumulators[upgrade_flaggroup] += upgrade_count << bundled_number
+
+ return accumulators
+
+
+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: SC2Context, mission_id: int):
+ sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
+
+ with DllDirectory(None):
+ run_game(bot.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
+ name="Archipelago", fullscreen=True)], realtime=True)
+
+
+class ArchipelagoBot(bot.bot_ai.BotAI):
+ game_running: bool = False
+ mission_completed: bool = False
+ boni: typing.List[bool]
+ setup_done: bool
+ ctx: SC2Context
+ mission_id: int
+ want_close: bool = False
+ can_read_game = False
+ last_received_update: int = 0
+
+ def __init__(self, ctx: SC2Context, mission_id):
+ self.setup_done = False
+ self.ctx = ctx
+ self.ctx.last_bot = self
+ self.mission_id = mission_id
+ self.boni = [False for _ in range(max_bonus)]
+
+ super(ArchipelagoBot, self).__init__()
+
+ async def on_step(self, iteration: int):
+ if self.want_close:
+ self.want_close = False
+ await self._client.leave()
+ return
+ game_state = 0
+ if not self.setup_done:
+ self.setup_done = True
+ start_items = calculate_items(self.ctx)
+ if self.ctx.difficulty_override >= 0:
+ difficulty = calc_difficulty(self.ctx.difficulty_override)
+ else:
+ difficulty = calc_difficulty(self.ctx.difficulty)
+ if self.ctx.game_speed_override >= 0:
+ game_speed = self.ctx.game_speed_override
+ else:
+ game_speed = self.ctx.game_speed
+ await self.chat_send("?SetOptions {} {} {} {}".format(
+ difficulty,
+ self.ctx.generic_upgrade_research,
+ self.ctx.all_in_choice,
+ game_speed
+ ))
+ await self.chat_send("?GiveResources {} {} {}".format(
+ start_items[8],
+ start_items[9],
+ start_items[10]
+ ))
+ await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {}".format(
+ start_items[0], start_items[1], start_items[2], start_items[3], start_items[4],
+ start_items[5], start_items[6], start_items[12], start_items[13], start_items[14]))
+ await self.chat_send("?GiveProtossTech {}".format(start_items[7]))
+ await self.chat_send("?SetColor rr " + str(self.ctx.player_color)) # TODO: Add faction color options
+ await self.chat_send("?LoadFinished")
+ self.last_received_update = len(self.ctx.items_received)
+
+ else:
+ if not self.ctx.announcements.empty():
+ message = self.ctx.announcements.get(timeout=1)
+ await self.chat_send("?SendMessage " + message)
+ self.ctx.announcements.task_done()
+
+ # Archipelago reads the health
+ for unit in self.all_own_units():
+ if unit.health_max == 38281:
+ game_state = int(38281 - unit.health)
+ self.can_read_game = True
+
+ if iteration == 160 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)
+ await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {}".format(
+ current_items[0], current_items[1], current_items[2], current_items[3], current_items[4],
+ current_items[5], current_items[6], current_items[12], current_items[13], current_items[14]))
+ await self.chat_send("?GiveProtossTech {}".format(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 self.can_read_game:
+ if game_state & (1 << 1) and not self.mission_completed:
+ if self.mission_id != self.ctx.final_mission:
+ print("Mission Completed")
+ await self.ctx.send_msgs(
+ [{"cmd": 'LocationChecks',
+ "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * 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
+
+ for x, completed in enumerate(self.boni):
+ if not completed and game_state & (1 << (x + 2)):
+ await self.ctx.send_msgs(
+ [{"cmd": 'LocationChecks',
+ "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}])
+ self.boni[x] = True
+
+ else:
+ await self.chat_send("?SendMessage LostConnection - Lost connection to game.")
+
+
+def request_unfinished_missions(ctx: SC2Context):
+ if ctx.mission_req_table:
+ message = "Unfinished Missions: "
+ unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
+ unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table)
+
+ _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
+
+ # Removing All-In from location pool
+ final_mission = lookup_id_to_mission[ctx.final_mission]
+ if final_mission in unfinished_missions.keys():
+ message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message
+ if unfinished_missions[final_mission] == -1:
+ unfinished_missions.pop(final_mission)
+
+ message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
+ mark_up_objectives(
+ f"[{len(unfinished_missions[mission])}/"
+ f"{sum(1 for _ in ctx.locations_for_mission(mission))}]",
+ ctx, unfinished_locations, mission)
+ for mission in unfinished_missions)
+
+ if ctx.ui:
+ ctx.ui.log_panels['All'].on_message_markup(message)
+ ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
+ else:
+ sc2_logger.info(message)
+ else:
+ sc2_logger.warning("No mission table found, you are likely not connected to a server.")
+
+
+def calc_unfinished_missions(ctx: SC2Context, unlocks=None):
+ unfinished_missions = []
+ locations_completed = []
+
+ if not unlocks:
+ unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
+
+ available_missions = calc_available_missions(ctx, unlocks)
+
+ for name in available_missions:
+ objectives = set(ctx.locations_for_mission(name))
+ if objectives:
+ objectives_completed = ctx.checked_locations & objectives
+ if len(objectives_completed) < len(objectives):
+ unfinished_missions.append(name)
+ locations_completed.append(objectives_completed)
+
+ else: # infer that this is the final mission as it has no objectives
+ unfinished_missions.append(name)
+ locations_completed.append(-1)
+
+ return available_missions, dict(zip(unfinished_missions, locations_completed))
+
+
+def is_mission_available(ctx: SC2Context, mission_id_to_check):
+ unfinished_missions = calc_available_missions(ctx)
+
+ return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions)
+
+
+def mark_up_mission_name(ctx: SC2Context, mission, unlock_table):
+ """Checks if the mission is required for game completion and adds '*' to the name to mark that."""
+
+ if ctx.mission_req_table[mission].completion_critical:
+ if ctx.ui:
+ message = "[color=AF99EF]" + mission + "[/color]"
+ else:
+ message = "*" + mission + "*"
+ else:
+ message = mission
+
+ if ctx.ui:
+ unlocks = unlock_table[mission]
+
+ if len(unlocks) > 0:
+ pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: "
+ pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks)
+ pre_message += f"]"
+ message = pre_message + message + "[/ref]"
+
+ return message
+
+
+def mark_up_objectives(message, ctx, unfinished_locations, mission):
+ formatted_message = message
+
+ if ctx.ui:
+ locations = unfinished_locations[mission]
+
+ pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|"
+ pre_message += "
".join(location for location in locations)
+ pre_message += f"]"
+ formatted_message = pre_message + message + "[/ref]"
+
+ return formatted_message
+
+
+def request_available_missions(ctx: SC2Context):
+ if ctx.mission_req_table:
+ message = "Available Missions: "
+
+ # Initialize mission unlock table
+ unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
+
+ missions = calc_available_missions(ctx, unlocks)
+ message += \
+ ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}"
+ f"[{ctx.mission_req_table[mission].id}]"
+ for mission in missions)
+
+ if ctx.ui:
+ ctx.ui.log_panels['All'].on_message_markup(message)
+ ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
+ else:
+ sc2_logger.info(message)
+ else:
+ sc2_logger.warning("No mission table found, you are likely not connected to a server.")
+
+
+def calc_available_missions(ctx: SC2Context, unlocks=None):
+ available_missions = []
+ missions_complete = 0
+
+ # Get number of missions completed
+ for loc in ctx.checked_locations:
+ if loc % victory_modulo == 0:
+ missions_complete += 1
+
+ for name in ctx.mission_req_table:
+ # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
+ if unlocks:
+ for unlock in ctx.mission_req_table[name].required_world:
+ unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name)
+
+ if mission_reqs_completed(ctx, name, missions_complete):
+ available_missions.append(name)
+
+ return available_missions
+
+
+def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int):
+ """Returns a bool signifying if the mission has all requirements complete and can be done
+
+ Arguments:
+ ctx -- instance of SC2Context
+ locations_to_check -- the mission string name to check
+ missions_complete -- an int of how many missions have been completed
+ mission_path -- a list of missions that have already been checked
+"""
+ if len(ctx.mission_req_table[mission_name].required_world) >= 1:
+ # A check for when the requirements are being or'd
+ or_success = False
+
+ # Loop through required missions
+ for req_mission in ctx.mission_req_table[mission_name].required_world:
+ req_success = True
+
+ # Check if required mission has been completed
+ if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id *
+ victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations:
+ if not ctx.mission_req_table[mission_name].or_requirements:
+ return False
+ else:
+ req_success = False
+
+ # Grid-specific logic (to avoid long path checks and infinite recursion)
+ if ctx.mission_order in (3, 4):
+ if req_success:
+ return True
+ else:
+ if req_mission is ctx.mission_req_table[mission_name].required_world[-1]:
+ return False
+ else:
+ continue
+
+ # Recursively check required mission to see if it's requirements are met, in case !collect has been done
+ # Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion
+ if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
+ if not ctx.mission_req_table[mission_name].or_requirements:
+ return False
+ else:
+ req_success = False
+
+ # If requirement check succeeded mark or as satisfied
+ if ctx.mission_req_table[mission_name].or_requirements and req_success:
+ or_success = True
+
+ if ctx.mission_req_table[mission_name].or_requirements:
+ # Return false if or requirements not met
+ if not or_success:
+ return False
+
+ # Check number of missions
+ if missions_complete >= ctx.mission_req_table[mission_name].number:
+ return True
+ else:
+ return False
+ else:
+ return True
+
+
+def initialize_blank_mission_dict(location_table):
+ unlocks = {}
+
+ for mission in list(location_table):
+ unlocks[mission] = []
+
+ return unlocks
+
+
+def check_game_install_path() -> bool:
+ # First thing: go to the default location for ExecuteInfo.
+ # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it.
+ if is_windows:
+ # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow.
+ # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555#
+ import ctypes.wintypes
+ CSIDL_PERSONAL = 5 # My Documents
+ SHGFP_TYPE_CURRENT = 0 # Get current, not default value
+
+ buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
+ ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
+ documentspath = buf.value
+ einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
+ else:
+ einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF]))
+
+ # Check if the file exists.
+ if os.path.isfile(einfo):
+
+ # Open the file and read it, picking out the latest executable's path.
+ with open(einfo) as f:
+ content = f.read()
+ if content:
+ try:
+ base = re.search(r" = (.*)Versions", content).group(1)
+ except AttributeError:
+ sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then "
+ f"try again.")
+ return False
+ if os.path.exists(base):
+ executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions")
+
+ # Finally, check the path for an actual executable.
+ # If we find one, great. Set up the SC2PATH.
+ if os.path.isfile(executable):
+ sc2_logger.info(f"Found an SC2 install at {base}!")
+ sc2_logger.debug(f"Latest executable at {executable}.")
+ os.environ["SC2PATH"] = base
+ sc2_logger.debug(f"SC2PATH set to {base}.")
+ return True
+ else:
+ sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.")
+ else:
+ sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
+ else:
+ sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. "
+ f"If that fails, please run /set_path with your SC2 install directory.")
+ return False
+
+
+def is_mod_installed_correctly() -> bool:
+ """Searches for all required files."""
+ if "SC2PATH" not in os.environ:
+ check_game_install_path()
+
+ mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign')
+ mods = ["ArchipelagoCore", "ArchipelagoPlayer", "ArchipelagoPlayerWoL", "ArchipelagoTriggers"]
+ modfiles = [os.environ["SC2PATH"] / Path("Mods/" + mod + ".SC2Mod") for mod in mods]
+ wol_required_maps = ["WoL" + os.sep + map_name + ".SC2Map" for map_name in maps_table]
+ needs_files = False
+
+ # Check for maps.
+ missing_maps = []
+ for mapfile in wol_required_maps:
+ if not os.path.isfile(mapdir / mapfile):
+ missing_maps.append(mapfile)
+ if len(missing_maps) >= 19:
+ sc2_logger.warning(f"All map files missing from {mapdir}.")
+ needs_files = True
+ elif len(missing_maps) > 0:
+ for map in missing_maps:
+ sc2_logger.debug(f"Missing {map} from {mapdir}.")
+ sc2_logger.warning(f"Missing {len(missing_maps)} map files.")
+ needs_files = True
+ else: # Must be no maps missing
+ sc2_logger.info(f"All maps found in {mapdir}.")
+
+ # Check for mods.
+ for modfile in modfiles:
+ if os.path.isfile(modfile) or os.path.isdir(modfile):
+ sc2_logger.info(f"Archipelago mod found at {modfile}.")
+ else:
+ sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.")
+ needs_files = True
+
+ # Final verdict.
+ if needs_files:
+ sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.")
+ return False
+ else:
+ sc2_logger.debug(f"All map/mod files are properly installed.")
+ return True
+
+
+class DllDirectory:
+ # Credit to Black Sliver for this code.
+ # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw
+ _old: typing.Optional[str] = None
+ _new: typing.Optional[str] = None
+
+ def __init__(self, new: typing.Optional[str]):
+ self._new = new
+
+ def __enter__(self):
+ old = self.get()
+ if self.set(self._new):
+ self._old = old
+
+ def __exit__(self, *args):
+ if self._old is not None:
+ self.set(self._old)
+
+ @staticmethod
+ def get() -> typing.Optional[str]:
+ if sys.platform == "win32":
+ n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
+ buf = ctypes.create_unicode_buffer(n)
+ ctypes.windll.kernel32.GetDllDirectoryW(n, buf)
+ return buf.value
+ # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
+ return None
+
+ @staticmethod
+ def set(s: typing.Optional[str]) -> bool:
+ if sys.platform == "win32":
+ return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0
+ # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
+ return False
+
+
+def download_latest_release_zip(owner: str, repo: str, api_version: str, metadata: str = None, force_download=False) -> (str, str):
+ """Downloads the latest release of a GitHub repo to the current directory as a .zip file."""
+ import requests
+
+ headers = {"Accept": 'application/vnd.github.v3+json'}
+ url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}"
+
+ r1 = requests.get(url, headers=headers)
+ if r1.status_code == 200:
+ latest_metadata = str(r1.json())
+ # sc2_logger.info(f"Latest version: {latest_metadata}.")
+ else:
+ sc2_logger.warning(f"Status code: {r1.status_code}")
+ sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.")
+ sc2_logger.warning(f"text: {r1.text}")
+ return "", metadata
+
+ if (force_download is False) and (metadata == latest_metadata):
+ sc2_logger.info("Latest version already installed.")
+ return "", metadata
+
+ sc2_logger.info(f"Attempting to download latest version of API version {api_version} of {repo}.")
+ download_url = r1.json()["assets"][0]["browser_download_url"]
+
+ r2 = requests.get(download_url, headers=headers)
+ if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
+ with open(f"{repo}.zip", "wb") as fh:
+ fh.write(r2.content)
+ sc2_logger.info(f"Successfully downloaded {repo}.zip.")
+ return f"{repo}.zip", latest_metadata
+ else:
+ sc2_logger.warning(f"Status code: {r2.status_code}")
+ sc2_logger.warning("Download failed.")
+ sc2_logger.warning(f"text: {r2.text}")
+ return "", metadata
+
+
+def is_mod_update_available(owner: str, repo: str, api_version: str, metadata: str) -> bool:
+ import requests
+
+ headers = {"Accept": 'application/vnd.github.v3+json'}
+ url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}"
+
+ r1 = requests.get(url, headers=headers)
+ if r1.status_code == 200:
+ latest_metadata = str(r1.json())
+ if metadata != latest_metadata:
+ return True
+ else:
+ return False
+
+ else:
+ sc2_logger.warning(f"Failed to reach GitHub while checking for updates.")
+ sc2_logger.warning(f"Status code: {r1.status_code}")
+ sc2_logger.warning(f"text: {r1.text}")
+ return False
+
+
+def launch():
+ colorama.init()
+ asyncio.run(main())
+ colorama.deinit()
diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py
index ea495adf..971a7537 100644
--- a/worlds/sc2wol/Items.py
+++ b/worlds/sc2wol/Items.py
@@ -12,6 +12,7 @@ class ItemData(typing.NamedTuple):
classification: ItemClassification = ItemClassification.useful
quantity: int = 1
parent_item: str = None
+ origin: typing.Set[str] = {"wol"}
class StarcraftWoLItem(Item):
@@ -43,23 +44,36 @@ item_table = {
"Ghost": ItemData(15 + SC2WOL_ITEM_ID_OFFSET, "Unit", 15, classification=ItemClassification.progression),
"Spectre": ItemData(16 + SC2WOL_ITEM_ID_OFFSET, "Unit", 16, classification=ItemClassification.progression),
"Thor": ItemData(17 + SC2WOL_ITEM_ID_OFFSET, "Unit", 17, classification=ItemClassification.progression),
+ # EE units
+ "Liberator": ItemData(18 + SC2WOL_ITEM_ID_OFFSET, "Unit", 18, classification=ItemClassification.progression, origin={"nco", "ext"}),
+ "Valkyrie": ItemData(19 + SC2WOL_ITEM_ID_OFFSET, "Unit", 19, classification=ItemClassification.progression, origin={"bw"}),
+ "Widow Mine": ItemData(20 + SC2WOL_ITEM_ID_OFFSET, "Unit", 20, classification=ItemClassification.progression, origin={"ext"}),
+ "Cyclone": ItemData(21 + SC2WOL_ITEM_ID_OFFSET, "Unit", 21, classification=ItemClassification.progression, origin={"ext"}),
+ # Some other items are moved to Upgrade group because of the way how the bot message is parsed
"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),
+ # Upgrade bundle 'number' values are used as indices to get affected 'number's
+ "Progressive Weapon Upgrade": ItemData(107 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3),
+ "Progressive Armor Upgrade": ItemData(108 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 1, quantity=3),
+ "Progressive Infantry Upgrade": ItemData(109 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3),
+ "Progressive Vehicle Upgrade": ItemData(110 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 3, quantity=3),
+ "Progressive Ship Upgrade": ItemData(111 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3),
+ "Progressive Weapon/Armor Upgrade": ItemData(112 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 5, quantity=3),
"Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, parent_item="Bunker"),
"Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, parent_item="Bunker"),
"Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler, parent_item="Missile Turret"),
"Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"),
- "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, parent_item="SCV"),
- "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, parent_item="SCV"),
+ "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, parent_item="Marine"),
+ "Progressive Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 0, parent_item="Marine", quantity=2),
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"),
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.filler, parent_item="Medic"),
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"),
@@ -69,10 +83,59 @@ item_table = {
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, parent_item="Marauder"),
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, parent_item="Reaper"),
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression, parent_item="Reaper"),
+ # Items from EE
+ "Mag-Field Accelerators (Cyclone)": ItemData(218 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 18, parent_item="Cyclone", origin={"ext"}),
+ "Mag-Field Launchers (Cyclone)": ItemData(219 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 19, parent_item="Cyclone", origin={"ext"}),
+ # Items from new mod
+ "Laser Targeting System (Marine)": ItemData(220 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, classification=ItemClassification.filler, parent_item="Marine", origin={"nco"}), # Freed slot from Stimpack
+ "Magrail Munitions (Marine)": ItemData(221 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 20, parent_item="Marine", origin={"nco"}),
+ "Optimized Logistics (Marine)": ItemData(222 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 21, classification=ItemClassification.filler, parent_item="Marine", origin={"nco"}),
+ "Restoration (Medic)": ItemData(223 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 22, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
+ "Optical Flare (Medic)": ItemData(224 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 23, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
+ "Optimized Logistics (Medic)": ItemData(225 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 24, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
+ "Progressive Stimpack (Firebat)": ItemData(226 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 6, parent_item="Firebat", quantity=2, origin={"bw"}),
+ "Optimized Logistics (Firebat)": ItemData(227 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 25, parent_item="Firebat", origin={"bw"}),
+ "Progressive Stimpack (Marauder)": ItemData(228 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 8, parent_item="Marauder", quantity=2, origin={"nco"}),
+ "Laser Targeting System (Marauder)": ItemData(229 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 26, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
+ "Magrail Munitions (Marauder)": ItemData(230 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 27, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
+ "Internal Tech Module (Marauder)": ItemData(231 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 28, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
+
+ # Items from new mod
+ "Progressive Stimpack (Reaper)": ItemData(250 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 10, parent_item="Reaper", quantity=2, origin={"nco"}),
+ "Laser Targeting System (Reaper)": ItemData(251 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 0, classification=ItemClassification.filler, parent_item="Reaper", origin={"nco"}),
+ "Advanced Cloaking Field (Reaper)": ItemData(252 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 1, parent_item="Reaper", origin={"nco"}),
+ "Spider Mines (Reaper)": ItemData(253 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 2, classification=ItemClassification.filler, parent_item="Reaper", origin={"nco"}),
+ "Combat Drugs (Reaper)": ItemData(254 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 3, classification=ItemClassification.filler, parent_item="Reaper", origin={"ext"}),
+ "Hellbat Aspect (Hellion)": ItemData(255 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 4, parent_item="Hellion", origin={"nco"}),
+ "Smart Servos (Hellion)": ItemData(256 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 5, parent_item="Hellion", origin={"nco"}),
+ "Optimized Logistics (Hellion)": ItemData(257 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 6, classification=ItemClassification.filler, parent_item="Hellion", origin={"nco"}),
+ "Jump Jets (Hellion)": ItemData(258 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 7, classification=ItemClassification.filler, parent_item="Hellion", origin={"nco"}),
+ "Progressive Stimpack (Hellion)": ItemData(259 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 12, parent_item="Hellion", quantity=2, origin={"nco"}),
+ "Ion Thrusters (Vulture)": ItemData(260 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 8, classification=ItemClassification.filler, parent_item="Vulture", origin={"bw"}),
+ "Auto Launchers (Vulture)": ItemData(261 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 9, parent_item="Vulture", origin={"bw"}),
+ "High Explosive Munition (Spider Mine)": ItemData(262 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 10, origin={"bw"}),
+ "Jump Jets (Goliath)": ItemData(263 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 11, classification=ItemClassification.filler, parent_item="Goliath", origin={"nco"}),
+ "Optimized Logistics (Goliath)": ItemData(264 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 12, classification=ItemClassification.filler, parent_item="Goliath", origin={"nco"}),
+ "Hyperfluxor (Diamondback)": ItemData(265 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 13, parent_item="Diamondback", origin={"ext"}),
+ "Burst Capacitors (Diamondback)": ItemData(266 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 14, classification=ItemClassification.filler, parent_item="Diamondback", origin={"ext"}),
+ "Optimized Logistics (Diamondback)": ItemData(267 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 15, parent_item="Diamondback", origin={"ext"}),
+ "Jump Jets (Siege Tank)": ItemData(268 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 16, parent_item="Siege Tank", origin={"nco"}),
+ "Spider Mines (Siege Tank)": ItemData(269 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 17, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
+ "Smart Servos (Siege Tank)": ItemData(270 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 18, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
+ "Graduating Range (Siege Tank)": ItemData(271 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 19, classification=ItemClassification.progression, parent_item="Siege Tank", origin={"ext"}),
+ "Laser Targeting System (Siege Tank)": ItemData(272 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 20, parent_item="Siege Tank", origin={"nco"}),
+ "Advanced Siege Tech (Siege Tank)": ItemData(273 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 21, parent_item="Siege Tank", origin={"ext"}),
+ "Internal Tech Module (Siege Tank)": ItemData(274 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 22, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
+ "Optimized Logistics (Predator)": ItemData(275 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 23, classification=ItemClassification.filler, parent_item="Predator", origin={"ext"}),
+ "Expanded Hull (Medivac)": ItemData(276 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 24, classification=ItemClassification.filler, parent_item="Medivac", origin={"ext"}),
+ "Afterburners (Medivac)": ItemData(277 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 25, classification=ItemClassification.filler, parent_item="Medivac", origin={"ext"}),
+ "Advanced Laser Technology (Wraith)": ItemData(278 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 26, classification=ItemClassification.progression, parent_item="Wraith", origin={"ext"}),
+ "Smart Servos (Viking)": ItemData(279 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 27, parent_item="Viking", origin={"ext"}),
+ "Magrail Munitions (Viking)": ItemData(280 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 28, parent_item="Viking", origin={"ext"}),
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler, parent_item="Hellion"),
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, parent_item="Hellion"),
- "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler, parent_item="Vulture"),
+ "Cerberus Mine (Spider Mine)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler),
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler, parent_item="Vulture"),
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, parent_item="Goliath"),
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, parent_item="Goliath"),
@@ -86,7 +149,7 @@ item_table = {
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler, parent_item="Wraith"),
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, parent_item="Viking"),
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, parent_item="Viking"),
- "Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler, parent_item="Banshee"),
+ "Progressive Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 2, classification=ItemClassification.filler, parent_item="Banshee", quantity=2),
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, parent_item="Banshee"),
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler, parent_item="Battlecruiser"),
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler, parent_item="Battlecruiser"),
@@ -96,6 +159,47 @@ item_table = {
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, parent_item="Spectre"),
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler, parent_item="Thor"),
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler, parent_item="Thor"),
+ # Items from EE
+ "Advanced Ballistics (Liberator)": ItemData(326 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 26, parent_item="Liberator", origin={"ext"}),
+ "Raid Artillery (Liberator)": ItemData(327 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 27, classification=ItemClassification.progression, parent_item="Liberator", origin={"nco"}),
+ "Drilling Claws (Widow Mine)": ItemData(328 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 28, classification=ItemClassification.filler, parent_item="Widow Mine", origin={"ext"}),
+ "Concealment (Widow Mine)": ItemData(329 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 29, classification=ItemClassification.progression, parent_item="Widow Mine", origin={"ext"}),
+
+ #Items from new mod
+ "Hyperflight Rotors (Banshee)": ItemData(350 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 0, classification=ItemClassification.filler, parent_item="Banshee", origin={"ext"}),
+ "Laser Targeting System (Banshee)": ItemData(351 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 1, classification=ItemClassification.filler, parent_item="Banshee", origin={"nco"}),
+ "Internal Tech Module (Banshee)": ItemData(352 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 2, classification=ItemClassification.filler, parent_item="Banshee", origin={"nco"}),
+ "Tactical Jump (Battlecruiser)": ItemData(353 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 3, parent_item="Battlecruiser", origin={"nco", "ext"}),
+ "Cloak (Battlecruiser)": ItemData(354 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 4, parent_item="Battlecruiser", origin={"nco"}),
+ "ATX Laser Battery (Battlecruiser)": ItemData(355 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 5, classification=ItemClassification.progression, parent_item="Battlecruiser", origin={"nco"}),
+ "Optimized Logistics (Battlecruiser)": ItemData(356 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 6, classification=ItemClassification.filler, parent_item="Battlecruiser", origin={"ext"}),
+ "Internal Tech Module (Battlecruiser)": ItemData(357 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 7, classification=ItemClassification.filler, parent_item="Battlecruiser", origin={"nco"}),
+ "EMP Rounds (Ghost)": ItemData(358 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 8, parent_item="Ghost", origin={"ext"}),
+ "Lockdown (Ghost)": ItemData(359 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 9, parent_item="Ghost", origin={"bw"}),
+ "Impaler Rounds (Spectre)": ItemData(360 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 10, parent_item="Spectre", origin={"ext"}),
+ "Progressive High Impact Payload (Thor)": ItemData(361 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 14, parent_item="Thor", quantity=2, origin={"ext"}), # L2 is Smart Servos
+ "Bio Mechanical Repair Drone (Raven)": ItemData(363 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 13, parent_item="Raven", origin={"nco"}),
+ "Spider Mines (Raven)": ItemData(364 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 14, parent_item="Raven", origin={"nco"}),
+ "Railgun Turret (Raven)": ItemData(365 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 15, parent_item="Raven", origin={"nco"}),
+ "Hunter-Seeker Weapon (Raven)": ItemData(366 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 16, parent_item="Raven", origin={"nco"}),
+ "Interference Matrix (Raven)": ItemData(367 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 17, parent_item="Raven", origin={"ext"}),
+ "Anti-Armor Missile (Raven)": ItemData(368 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 18, classification=ItemClassification.filler, parent_item="Raven", origin={"ext"}),
+ "Internal Tech Module (Raven)": ItemData(369 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 19, classification=ItemClassification.filler, parent_item="Raven", origin={"nco"}),
+ "EMP Shockwave (Science Vessel)": ItemData(370 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 20, parent_item="Science Vessel", origin={"bw"}),
+ "Defensive Matrix (Science Vessel)": ItemData(371 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 21, parent_item="Science Vessel", origin={"bw"}),
+ "Targeting Optics (Cyclone)": ItemData(372 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 22, parent_item="Cyclone", origin={"ext"}),
+ "Rapid Fire Launchers (Cyclone)": ItemData(373 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 23, parent_item="Cyclone", origin={"ext"}),
+ "Cloak (Liberator)": ItemData(374 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 24, classification=ItemClassification.filler, parent_item="Liberator", origin={"nco"}),
+ "Laser Targeting System (Liberator)": ItemData(375 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 25, classification=ItemClassification.filler, parent_item="Liberator", origin={"ext"}),
+ "Optimized Logistics (Liberator)": ItemData(376 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 26, classification=ItemClassification.filler, parent_item="Liberator", origin={"nco"}),
+ "Black Market Launchers (Widow Mine)": ItemData(377 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 27, classification=ItemClassification.filler, parent_item="Widow Mine", origin={"ext"}),
+ "Executioner Missiles (Widow Mine)": ItemData(378 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 28, parent_item="Widow Mine", origin={"ext"}),
+
+ # Just lazy to create a new group for one unit
+ "Enhanced Cluster Launchers (Valkyrie)": ItemData(379 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, parent_item="Valkyrie", origin={"ext"}),
+ "Shaped Hull (Valkyrie)": ItemData(380 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 20, classification=ItemClassification.filler, parent_item="Valkyrie", origin={"ext"}),
+ "Burst Lasers (Valkyrie)": ItemData(381 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 21, parent_item="Valkyrie", origin={"ext"}),
+ "Afterburners (Valkyrie)": ItemData(382 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 22, classification=ItemClassification.filler, parent_item="Valkyrie", origin={"ext"}),
"Bunker": ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, classification=ItemClassification.progression),
"Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression),
@@ -120,14 +224,14 @@ item_table = {
"Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression),
"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, parent_item="Bunker"),
- "Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"),
+ "Shrike Turret (Bunker)": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"),
+ "Fortified Bunker (Bunker)": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"),
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression),
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression),
"Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler),
"Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression),
"Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16),
- "Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17),
+ "Progressive Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 4, quantity=2),
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression),
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression),
@@ -136,18 +240,24 @@ item_table = {
"High Templar": ItemData(702 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 2, classification=ItemClassification.progression),
"Dark Templar": ItemData(703 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 3, classification=ItemClassification.progression),
"Immortal": ItemData(704 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 4, classification=ItemClassification.progression),
- "Colossus": ItemData(705 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 5, classification=ItemClassification.progression),
- "Phoenix": ItemData(706 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 6, classification=ItemClassification.progression),
+ "Colossus": ItemData(705 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 5),
+ "Phoenix": ItemData(706 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 6, classification=ItemClassification.filler),
"Void Ray": ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, classification=ItemClassification.progression),
"Carrier": ItemData(708 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 8, classification=ItemClassification.progression),
+ # Filler items to fill remaining spots
"+15 Starting Minerals": ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, classification=ItemClassification.filler),
"+15 Starting Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, classification=ItemClassification.filler),
+ # This Filler item isn't placed by the generator yet unless plando'd
"+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler),
+ # This item is used to "remove" location from the game. Never placed unless plando'd
+ "Nothing": ItemData(803 + SC2WOL_ITEM_ID_OFFSET, "Nothing Group", 2, quantity=0, classification=ItemClassification.trap),
# "Keystone Piece": ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing)
}
+def get_item_table(multiworld: MultiWorld, player: int):
+ return item_table
basic_units = {
'Marine',
@@ -172,10 +282,49 @@ def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]:
item_name_groups = {}
-for item, data in item_table.items():
+for item, data in get_full_item_list().items():
item_name_groups.setdefault(data.type, []).append(item)
+ if data.type in ("Armory 1", "Armory 2") and '(' in item:
+ short_name = item[:item.find(' (')]
+ item_name_groups[short_name] = [item]
item_name_groups["Missions"] = ["Beat " + mission_name for mission_name in vanilla_mission_req_table]
+
+# Items that can be placed before resources if not already in
+# General upgrades and Mercs
+second_pass_placeable_items: typing.Tuple[str, ...] = (
+ # Buildings without upgrades
+ "Sensor Tower",
+ "Hive Mind Emulator",
+ "Psi Disrupter",
+ "Perdition Turret",
+ # General upgrades without any dependencies
+ "Advanced Construction (SCV)",
+ "Dual-Fusion Welders (SCV)",
+ "Fire-Suppression System (Building)",
+ "Orbital Command (Building)",
+ "Ultra-Capacitors",
+ "Vanadium Plating",
+ "Orbital Depots",
+ "Micro-Filtering",
+ "Automated Refinery",
+ "Command Center Reactor",
+ "Tech Reactor",
+ "Planetary Fortress",
+ "Cellular Reactor",
+ "Progressive Regenerative Bio-Steel", # Place only L1
+ # Mercenaries
+ "War Pigs",
+ "Devil Dogs",
+ "Hammer Securities",
+ "Spartan Company",
+ "Siege Breakers",
+ "Hel's Angel",
+ "Dusk Wings",
+ "Jackson's Revenge"
+)
+
+
filler_items: typing.Tuple[str, ...] = (
'+15 Starting Minerals',
'+15 Starting Vespene'
@@ -190,7 +339,10 @@ defense_ratings = {
# Bunker w/ Marine/Marauder: 3,
"Perdition Turret": 2,
"Missile Turret": 2,
- "Vulture": 2
+ "Vulture": 2,
+ "Liberator": 2,
+ "Widow Mine": 2
+ # "Concealment (Widow Mine)": 1
}
zerg_defense_ratings = {
"Perdition Turret": 2,
@@ -199,14 +351,61 @@ zerg_defense_ratings = {
"Psi Disruptor": 3
}
+spider_mine_sources = {
+ "Vulture",
+ "Spider Mines (Reaper)",
+ "Spider Mines (Siege Tank)",
+ "Spider Mines (Raven)"
+}
+
+progressive_if_nco = {
+ "Progressive Stimpack (Marine)",
+ "Progressive Stimpack (Firebat)",
+ "Progressive Cross-Spectrum Dampeners (Banshee)",
+ "Progressive Regenerative Bio-Steel"
+}
+
+# 'number' values of upgrades for upgrade bundle items
+upgrade_numbers = [
+ {0, 4, 8}, # Weapon
+ {2, 6, 10}, # Armor
+ {0, 2}, # Infantry
+ {4, 6}, # Vehicle
+ {8, 10}, # Starship
+ {0, 2, 4, 6, 8, 10} # All
+]
+# Names of upgrades to be included for different options
+upgrade_included_names = [
+ { # Individual Items
+ "Progressive Infantry Weapon",
+ "Progressive Infantry Armor",
+ "Progressive Vehicle Weapon",
+ "Progressive Vehicle Armor",
+ "Progressive Ship Weapon",
+ "Progressive Ship Armor"
+ },
+ { # Bundle Weapon And Armor
+ "Progressive Weapon Upgrade",
+ "Progressive Armor Upgrade"
+ },
+ { # Bundle Unit Class
+ "Progressive Infantry Upgrade",
+ "Progressive Vehicle Upgrade",
+ "Progressive Starship Upgrade"
+ },
+ { # Bundle All
+ "Progressive Weapon/Armor Upgrade"
+ }
+]
+
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}
# Map type to expected int
type_flaggroups: typing.Dict[str, int] = {
"Unit": 0,
- "Upgrade": 1,
- "Armory 1": 2,
- "Armory 2": 3,
+ "Upgrade": 1, # Weapon / Armor upgrades
+ "Armory 1": 2, # Unit upgrades
+ "Armory 2": 3, # Unit upgrades
"Building": 4,
"Mercenary": 5,
"Laboratory": 6,
@@ -214,5 +413,9 @@ type_flaggroups: typing.Dict[str, int] = {
"Minerals": 8,
"Vespene": 9,
"Supply": 10,
- "Goal": 11
+ "Goal": 11,
+ "Armory 3": 12, # Unit upgrades
+ "Armory 4": 13, # Unit upgrades
+ "Progressive Upgrade": 14, # Unit upgrades that exist multiple times (Stimpack / Super Stimpack)
+ "Nothing Group": 15
}
diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py
index e91068c4..ae31fa8e 100644
--- a/worlds/sc2wol/Locations.py
+++ b/worlds/sc2wol/Locations.py
@@ -1,3 +1,4 @@
+from enum import IntEnum
from typing import List, Tuple, Optional, Callable, NamedTuple
from BaseClasses import MultiWorld
from .Options import get_option_value
@@ -11,10 +12,18 @@ class SC2WoLLocation(Location):
game: str = "Starcraft2WoL"
+class LocationType(IntEnum):
+ VICTORY = 0 # Winning a mission
+ MISSION_PROGRESS = 1 # All tasks done for progressing the mission normally towards victory. All cleaning of expansion bases falls here
+ BONUS = 2 # Bonus objective, getting a campaign or mission bonus in vanilla (credits, research, bonus units or resources)
+ CHALLENGE = 3 # Challenging objectives, often harder than just completing a mission
+ OPTIONAL_BOSS = 4 # Any boss that's not required to win the mission. All Brutalisks, Loki, etc.
+
class LocationData(NamedTuple):
region: str
name: str
code: Optional[int]
+ type: LocationType
rule: Callable = lambda state: True
@@ -22,256 +31,473 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu
# Note: rules which are ended with or True are rules identified as needed later when restricted units is an option
logic_level = get_option_value(multiworld, player, 'required_tactics')
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("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200,
+ LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100, LocationType.VICTORY),
+ LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Special Delivery", SC2WOL_LOC_ID_OFFSET + 107, LocationType.MISSION_PROGRESS),
+ LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201,
+ LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300,
+ LocationData("The Outlaws", "The Outlaws: North Resource Pickups", SC2WOL_LOC_ID_OFFSET + 202, LocationType.BONUS),
+ LocationData("The Outlaws", "The Outlaws: Bunker", SC2WOL_LOC_ID_OFFSET + 203, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player)),
+ LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 2 and
(logic_level > 0 or state._sc2wol_has_anti_air(multiworld, player))),
- 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: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301, LocationType.BONUS),
+ LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303,
+ LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 2),
- LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400,
+ LocationData("Zero Hour", "Zero Hour: First Hatchery", SC2WOL_LOC_ID_OFFSET + 304, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Zero Hour", "Zero Hour: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 305, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Zero Hour", "Zero Hour: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 306, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Zero Hour", "Zero Hour: Fourth Hatchery", SC2WOL_LOC_ID_OFFSET + 307, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401),
- LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402,
+ LocationData("Evacuation", "Evacuation: First Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS),
+ LocationData("Evacuation", "Evacuation: Second Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403,
+ LocationData("Evacuation", "Evacuation: Third Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500,
+ LocationData("Evacuation", "Evacuation: Reach Hanson", SC2WOL_LOC_ID_OFFSET + 404, LocationType.MISSION_PROGRESS),
+ LocationData("Evacuation", "Evacuation: Secret Resource Stash", SC2WOL_LOC_ID_OFFSET + 405, LocationType.BONUS),
+ LocationData("Evacuation", "Evacuation: Flawless", SC2WOL_LOC_ID_OFFSET + 406, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
+ or state._sc2wol_has_competent_anti_air(multiworld, player))),
+ LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, LocationType.VICTORY,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 4 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501,
+ LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502,
+ LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600,
+ LocationData("Outbreak", "Outbreak: North Infested Command Center", SC2WOL_LOC_ID_OFFSET + 503, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Outbreak", "Outbreak: South Infested Command Center", SC2WOL_LOC_ID_OFFSET + 504, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Outbreak", "Outbreak: Northwest Bar", SC2WOL_LOC_ID_OFFSET + 505, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Outbreak", "Outbreak: North Bar", SC2WOL_LOC_ID_OFFSET + 506, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Outbreak", "Outbreak: South Bar", SC2WOL_LOC_ID_OFFSET + 507, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601,
+ LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602,
+ LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603,
+ LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700,
+ LocationData("Safe Haven", "Safe Haven: First Terror Fleet", SC2WOL_LOC_ID_OFFSET + 604, LocationType.BONUS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ state._sc2wol_has_competent_anti_air(multiworld, player)),
+ LocationData("Safe Haven", "Safe Haven: Second Terror Fleet", SC2WOL_LOC_ID_OFFSET + 605, LocationType.BONUS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ state._sc2wol_has_competent_anti_air(multiworld, player)),
+ LocationData("Safe Haven", "Safe Haven: Third Terror Fleet", SC2WOL_LOC_ID_OFFSET + 606, LocationType.BONUS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ state._sc2wol_has_competent_anti_air(multiworld, player)),
+ LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701,
+ LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702,
+ LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703,
+ LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800,
+ LocationData("Haven's Fall", "Haven's Fall: Northeast Colony Base", SC2WOL_LOC_ID_OFFSET + 704, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_can_respond_to_colony_infestations),
+ LocationData("Haven's Fall", "Haven's Fall: East Colony Base", SC2WOL_LOC_ID_OFFSET + 705, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_can_respond_to_colony_infestations),
+ LocationData("Haven's Fall", "Haven's Fall: Middle Colony Base", SC2WOL_LOC_ID_OFFSET + 706, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_can_respond_to_colony_infestations),
+ LocationData("Haven's Fall", "Haven's Fall: Southeast Colony Base", SC2WOL_LOC_ID_OFFSET + 707, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_can_respond_to_colony_infestations),
+ LocationData("Haven's Fall", "Haven's Fall: Southwest Colony Base", SC2WOL_LOC_ID_OFFSET + 708, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_can_respond_to_colony_infestations),
+ LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
- 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: First Relic", SC2WOL_LOC_ID_OFFSET + 801, LocationType.BONUS),
+ LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802, LocationType.BONUS),
+ LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804,
+ LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900,
+ LocationData("Smash and Grab", "Smash and Grab: First Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 805, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
+ or state._sc2wol_has_competent_anti_air(multiworld, player))),
+ LocationData("Smash and Grab", "Smash and Grab: Second Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 806, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
+ or state._sc2wol_has_competent_anti_air(multiworld, player))),
+ LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, LocationType.VICTORY,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, False) >= 7),
- LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901,
+ LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
- LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902,
+ LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
- LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903,
+ LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
- LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
+ LocationData("The Dig", "The Dig: Moebius Base", SC2WOL_LOC_ID_OFFSET + 904, LocationType.MISSION_PROGRESS),
+ LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, LocationType.VICTORY,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
(state._sc2wol_has_air(multiworld, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(multiworld, player))),
- LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
+ LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core", SC2WOL_LOC_ID_OFFSET + 1001, LocationType.MISSION_PROGRESS),
+ LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, LocationType.MISSION_PROGRESS,
+ lambda state: (state._sc2wol_has_air(multiworld, player)
+ or state.has_any({'Medivac', 'Hercules'}, player)
+ and state._sc2wol_has_common_unit(multiworld, player))),
+ LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
+ LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005,
+ LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006,
+ LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
+ LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
+ LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
(state._sc2wol_has_air(multiworld, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(multiworld, player))),
- LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100,
+ LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1009, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_anti_air(multiworld, player) and
+ (state._sc2wol_has_air(multiworld, player)
+ or state.has_any({'Medivac', 'Hercules'}, player)
+ and state._sc2wol_has_common_unit(multiworld, player))),
+ LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, LocationType.VICTORY,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- 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: West Relic", SC2WOL_LOC_ID_OFFSET + 1101, LocationType.BONUS),
+ LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, LocationType.BONUS),
+ LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, LocationType.BONUS,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104,
+ LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, LocationType.BONUS,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200,
+ LocationData("Supernova", "Supernova: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1105, LocationType.MISSION_PROGRESS),
+ LocationData("Supernova", "Supernova: Middle Base", SC2WOL_LOC_ID_OFFSET + 1106, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Supernova", "Supernova: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1107, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, LocationType.VICTORY,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- 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: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201, LocationType.MISSION_PROGRESS),
+ LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
- 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 Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
- 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: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, LocationType.BONUS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205,
+ LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, LocationType.BONUS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300,
+ LocationData("Maw of the Void", "Maw of the Void: Mothership", SC2WOL_LOC_ID_OFFSET + 1206, LocationType.OPTIONAL_BOSS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Expansion Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1207, LocationType.MISSION_PROGRESS,
+ lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Middle Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1208, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Southeast Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1209, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Stargate Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1210, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Northwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1211, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: West Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1212, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Southwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1213, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, LocationType.VICTORY,
lambda state: logic_level > 0 or
state._sc2wol_has_anti_air(multiworld, player) and (
state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- 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", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301, LocationType.BONUS),
+ LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, LocationType.OPTIONAL_BOSS,
lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- 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,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Breakout", "Breakout: Victory", 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("Ghost of a Chance", "Ghost of a Chance: Victory", 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("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700,
+ LocationData("Devil's Playground", "Devil's Playground: North Reapers", SC2WOL_LOC_ID_OFFSET + 1303, LocationType.BONUS),
+ LocationData("Devil's Playground", "Devil's Playground: Middle Reapers", SC2WOL_LOC_ID_OFFSET + 1304, LocationType.BONUS,
+ lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
+ LocationData("Devil's Playground", "Devil's Playground: Southwest Reapers", SC2WOL_LOC_ID_OFFSET + 1305, LocationType.BONUS,
+ lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
+ LocationData("Devil's Playground", "Devil's Playground: Southeast Reapers", SC2WOL_LOC_ID_OFFSET + 1306, LocationType.BONUS,
+ lambda state: logic_level > 0 or
+ state._sc2wol_has_anti_air(multiworld, player) and (
+ state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Devil's Playground", "Devil's Playground: East Reapers", SC2WOL_LOC_ID_OFFSET + 1307, LocationType.BONUS,
+ lambda state: state._sc2wol_has_anti_air(multiworld, player) and
+ (logic_level > 0 or
+ state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, LocationType.VICTORY,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401, LocationType.BONUS),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, LocationType.BONUS,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, LocationType.BONUS,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Middle Base", SC2WOL_LOC_ID_OFFSET + 1404, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Main Base", SC2WOL_LOC_ID_OFFSET + 1405, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
+ and state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: No Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1406, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
+ and state._sc2wol_has_competent_ground_to_air(multiworld, player)
+ and state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 1 Terrazine Node Sealed", SC2WOL_LOC_ID_OFFSET + 1407, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
+ and state._sc2wol_has_competent_ground_to_air(multiworld, player)
+ and state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 2 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1408, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
+ and state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 3 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1409, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
+ and state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 4 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1410, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 5 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1411, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500, LocationType.VICTORY),
+ LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501, LocationType.BONUS),
+ LocationData("Breakout", "Breakout: Siege Tank Prison", SC2WOL_LOC_ID_OFFSET + 1502, LocationType.BONUS),
+ LocationData("Breakout", "Breakout: First Checkpoint", SC2WOL_LOC_ID_OFFSET + 1503, LocationType.MISSION_PROGRESS),
+ LocationData("Breakout", "Breakout: Second Checkpoint", SC2WOL_LOC_ID_OFFSET + 1504, LocationType.MISSION_PROGRESS),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600, LocationType.VICTORY),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601, LocationType.MISSION_PROGRESS),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602, LocationType.MISSION_PROGRESS),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603, LocationType.BONUS),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604, LocationType.BONUS),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, LocationType.VICTORY,
lambda state: state._sc2wol_has_train_killers(multiworld, player) and
state._sc2wol_has_anti_air(multiworld, 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("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800,
+ LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Close Diamondback", SC2WOL_LOC_ID_OFFSET + 1704, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Northwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1705, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: North Diamondback", SC2WOL_LOC_ID_OFFSET + 1706, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Northeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1707, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Southwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1708, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Southeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1709, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Kill Team", SC2WOL_LOC_ID_OFFSET + 1710, LocationType.CHALLENGE,
+ lambda state: (logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player)) and
+ state._sc2wol_has_train_killers(multiworld, player) and
+ state._sc2wol_has_anti_air(multiworld, player)),
+ LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 or state._sc2wol_has_anti_air)),
- LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801,
+ LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802,
+ LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803),
- LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804,
+ LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803, LocationType.BONUS),
+ LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900,
+ LocationData("Cutthroat", "Cutthroat: North Command Center", SC2WOL_LOC_ID_OFFSET + 1805, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player)),
+ LocationData("Cutthroat", "Cutthroat: South Command Center", SC2WOL_LOC_ID_OFFSET + 1806, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player)),
+ LocationData("Cutthroat", "Cutthroat: West Command Center", SC2WOL_LOC_ID_OFFSET + 1807, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, LocationType.VICTORY,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', 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,
+ LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901, LocationType.MISSION_PROGRESS),
+ LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', 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,
+ LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903, LocationType.BONUS),
+ LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905,
+ LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000,
+ LocationData("Engine of Destruction", "Engine of Destruction: West Base", SC2WOL_LOC_ID_OFFSET + 1906, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
+ state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Northwest Base", SC2WOL_LOC_ID_OFFSET + 1907, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
+ state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Northeast Base", SC2WOL_LOC_ID_OFFSET + 1908, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
+ state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1909, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
+ state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
+ LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, LocationType.VICTORY,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001,
+ LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002,
+ LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003,
+ LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100,
+ LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004, LocationType.BONUS),
+ LocationData("Media Blitz", "Media Blitz: All Barracks", SC2WOL_LOC_ID_OFFSET + 2005, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Media Blitz", "Media Blitz: All Factories", SC2WOL_LOC_ID_OFFSET + 2006, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Media Blitz", "Media Blitz: All Starports", SC2WOL_LOC_ID_OFFSET + 2007, LocationType.MISSION_PROGRESS,
+ lambda state: logic_level > 0 or state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Media Blitz", "Media Blitz: Odin Not Trashed", SC2WOL_LOC_ID_OFFSET + 2008, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, LocationType.VICTORY,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- 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: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101, LocationType.BONUS),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, LocationType.BONUS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103,
+ LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103,LocationType.BONUS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104,
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, LocationType.BONUS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105,
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Whispers of Doom", "Whispers of Doom: Victory", 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("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300,
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Fusion Reactor", SC2WOL_LOC_ID_OFFSET + 2106, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
+ LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200, LocationType.VICTORY),
+ LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201, LocationType.BONUS),
+ LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202, LocationType.BONUS),
+ LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203, LocationType.BONUS),
+ LocationData("Whispers of Doom", "Whispers of Doom: First Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2204, LocationType.MISSION_PROGRESS),
+ LocationData("Whispers of Doom", "Whispers of Doom: Second Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2205, LocationType.MISSION_PROGRESS),
+ LocationData("Whispers of Doom", "Whispers of Doom: Third Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2206, LocationType.MISSION_PROGRESS),
+ LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, LocationType.VICTORY,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301,
+ LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302,
+ LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303,
- lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400,
+ LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, LocationType.BONUS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Northeast Base", SC2WOL_LOC_ID_OFFSET + 2304, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Southeast Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Maar", SC2WOL_LOC_ID_OFFSET + 2306, LocationType.MISSION_PROGRESS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)),
- 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,
- lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500),
- LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501,
+ LocationData("A Sinister Turn", "A Sinister Turn: Northwest Preserver", SC2WOL_LOC_ID_OFFSET + 2307, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502,
+ LocationData("A Sinister Turn", "A Sinister Turn: Southwest Preserver", SC2WOL_LOC_ID_OFFSET + 2308, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("A Sinister Turn", "A Sinister Turn: East Preserver", SC2WOL_LOC_ID_OFFSET + 2309, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, LocationType.VICTORY,
+ lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401, LocationType.BONUS),
+ LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, LocationType.BONUS,
+ lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Base", SC2WOL_LOC_ID_OFFSET + 2403, LocationType.MISSION_PROGRESS),
+ LocationData("Echoes of the Future", "Echoes of the Future: Southwest Tendril", SC2WOL_LOC_ID_OFFSET + 2404, LocationType.MISSION_PROGRESS),
+ LocationData("Echoes of the Future", "Echoes of the Future: Southeast Tendril", SC2WOL_LOC_ID_OFFSET + 2405, LocationType.MISSION_PROGRESS,
+ lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Northeast Tendril", SC2WOL_LOC_ID_OFFSET + 2406, LocationType.MISSION_PROGRESS,
+ lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Northwest Tendril", SC2WOL_LOC_ID_OFFSET + 2407, LocationType.MISSION_PROGRESS,
+ lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500, LocationType.VICTORY),
+ LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, LocationType.BONUS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, LocationType.CHALLENGE,
lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600,
+ LocationData("In Utter Darkness", "In Utter Darkness: Urun", SC2WOL_LOC_ID_OFFSET + 2503, LocationType.MISSION_PROGRESS),
+ LocationData("In Utter Darkness", "In Utter Darkness: Mohandar", SC2WOL_LOC_ID_OFFSET + 2504, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Selendis", SC2WOL_LOC_ID_OFFSET + 2505, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Artanis", SC2WOL_LOC_ID_OFFSET + 2506, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, LocationType.VICTORY,
lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) > 6),
- LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601,
+ LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) > 6),
- LocationData("Belly of the Beast", "Belly of the Beast: Victory", 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("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800,
+ LocationData("Gates of Hell", "Gates of Hell: 2 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2602, LocationType.BONUS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
+ state._sc2wol_defense_rating(multiworld, player, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: 4 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2603, LocationType.BONUS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
+ state._sc2wol_defense_rating(multiworld, player, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: 6 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2604, LocationType.BONUS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
+ state._sc2wol_defense_rating(multiworld, player, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: 8 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2605, LocationType.BONUS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
+ state._sc2wol_defense_rating(multiworld, player, True) > 6),
+ LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700, LocationType.VICTORY),
+ LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701, LocationType.MISSION_PROGRESS),
+ LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702, LocationType.MISSION_PROGRESS),
+ LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703, LocationType.MISSION_PROGRESS),
+ LocationData("Belly of the Beast", "Belly of the Beast: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 2704, LocationType.BONUS),
+ LocationData("Belly of the Beast", "Belly of the Beast: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 2705, LocationType.BONUS),
+ LocationData("Belly of the Beast", "Belly of the Beast: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 2706, LocationType.BONUS),
+ LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800, LocationType.VICTORY,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801,
+ LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802,
+ LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803,
+ LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804,
+ LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805,
+ LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("All-In", "All-In: Victory", None,
+ LocationData("Shatter the Sky", "Shatter the Sky: East Hatchery", SC2WOL_LOC_ID_OFFSET + 2806, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Shatter the Sky", "Shatter the Sky: North Hatchery", SC2WOL_LOC_ID_OFFSET + 2807, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Shatter the Sky", "Shatter the Sky: Mid Hatchery", SC2WOL_LOC_ID_OFFSET + 2808, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("All-In", "All-In: Victory", None, LocationType.VICTORY,
lambda state: state._sc2wol_final_mission_requirements(multiworld, player))
]
diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py
index 8c7182e0..112302be 100644
--- a/worlds/sc2wol/LogicMixin.py
+++ b/worlds/sc2wol/LogicMixin.py
@@ -9,22 +9,38 @@ class SC2WoLLogic(LogicMixin):
return self.has_any(get_basic_units(multiworld, player), player)
def _sc2wol_has_air(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 \
+ return self.has_any({'Viking', 'Wraith', 'Banshee', 'Battlecruiser'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 \
and self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(multiworld, player)
def _sc2wol_has_air_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
return self.has('Viking', player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Wraith', player)
+ or self.has_all({'Wraith', 'Advanced Laser Technology (Wraith)'}, player) \
+ or self.has_all({'Battlecruiser', 'ATX Laser Battery (Battlecruiser)'}, player) \
+ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Wraith', 'Valkyrie', 'Battlecruiser'}, player)
- def _sc2wol_has_competent_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
+ def _sc2wol_has_competent_ground_to_air(self, multiworld: MultiWorld, player: int) -> bool:
return self.has('Goliath', player) \
or self.has('Marine', player) and self.has_any({'Medic', 'Medivac'}, player) \
+ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Cyclone', player)
+
+ def _sc2wol_has_competent_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
+ return self._sc2wol_has_competent_ground_to_air(multiworld, player) \
or self._sc2wol_has_air_anti_air(multiworld, player)
+ def _sc2wol_welcome_to_the_jungle_requirement(self, multiworld: MultiWorld, player: int) -> bool:
+ return (
+ self._sc2wol_has_common_unit(multiworld, player)
+ and self._sc2wol_has_competent_ground_to_air(multiworld, player)
+ ) or (
+ get_option_value(multiworld, player, 'required_tactics') > 0
+ and self.has_any({'Marine', 'Vulture'}, player)
+ and self._sc2wol_has_air_anti_air(multiworld, player)
+ )
+
def _sc2wol_has_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Marine', 'Wraith'}, player) \
+ return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Marine', 'Wraith', 'Valkyrie', 'Cyclone'}, player) \
or self._sc2wol_has_competent_anti_air(multiworld, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
+ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre', 'Widow Mine', 'Liberator'}, player)
def _sc2wol_defense_rating(self, multiworld: MultiWorld, player: int, zerg_enemy: bool, air_enemy: bool = True) -> bool:
defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player)))
@@ -32,6 +48,10 @@ class SC2WoLLogic(LogicMixin):
defense_score += 3
if self.has_all({'Siege Tank', 'Maelstrom Rounds (Siege Tank)'}, player):
defense_score += 2
+ if self.has_all({'Siege Tank', 'Graduating Range (Siege Tank)'}, player):
+ defense_score += 1
+ if self.has_all({'Widow Mine', 'Concealment (Widow Mine)'}, player):
+ defense_score += 1
if zerg_enemy:
defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player)))
if self.has('Firebat', player) and self.has('Bunker', player):
@@ -44,20 +64,27 @@ class SC2WoLLogic(LogicMixin):
return defense_score
def _sc2wol_has_competent_comp(self, multiworld: MultiWorld, player: int) -> bool:
- return (self.has('Marine', player) or self.has('Marauder', player) and
- self._sc2wol_has_competent_anti_air(multiworld, player)) and self.has_any({'Medivac', 'Medic'}, player) or \
- self.has('Thor', player) or self.has("Banshee", player) and self._sc2wol_has_competent_anti_air(multiworld, player) or \
- self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(multiworld, player) or \
- self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(multiworld, player)
+ return \
+ (
+ (
+ self.has_any({'Marine', 'Marauder'}, player) and self.has_any({'Medivac', 'Medic'}, player)
+ or self.has_any({'Thor', 'Banshee', 'Siege Tank'}, player)
+ or self.has_all({'Liberator', 'Raid Artillery (Liberator)'}, player)
+ ) and self._sc2wol_has_competent_anti_air(multiworld, player)
+ ) \
+ or \
+ (
+ self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(multiworld, player)
+ )
def _sc2wol_has_train_killers(self, multiworld: MultiWorld, player: int) -> bool:
return (
- self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player)
+ self.has_any({'Siege Tank', 'Diamondback', 'Marauder', 'Cyclone'}, player)
or get_option_value(multiworld, player, 'required_tactics') > 0
and (
self.has_all({'Reaper', "G-4 Clusterbomb"}, player)
or self.has_all({'Spectre', 'Psionic Lash'}, player)
- or self.has('Vulture', player)
+ or self.has_any({'Vulture', 'Liberator'}, player)
)
)
@@ -66,16 +93,18 @@ class SC2WoLLogic(LogicMixin):
def _sc2wol_has_protoss_common_units(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
+ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('High Templar', player)
def _sc2wol_has_protoss_medium_units(self, multiworld: MultiWorld, player: int) -> bool:
return self._sc2wol_has_protoss_common_units(multiworld, player) and \
- self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
+ self.has_any({'Stalker', 'Void Ray', 'Carrier'}, player) \
+ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Dark Templar', player)
def _sc2wol_beats_protoss_deathball(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air(multiworld, player) or \
- self._sc2wol_has_competent_comp(multiworld, player) and self._sc2wol_has_air_anti_air(multiworld, player)
+ return (self.has_any({'Banshee', 'Battlecruiser'}, player) or
+ self.has_all({'Liberator', 'Raid Artillery (Liberator)'}, player)) \
+ and self._sc2wol_has_competent_anti_air(multiworld, player) or \
+ self._sc2wol_has_competent_comp(multiworld, player) and self._sc2wol_has_air_anti_air(multiworld, player)
def _sc2wol_has_mm_upgrade(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player)
@@ -89,6 +118,17 @@ class SC2WoLLogic(LogicMixin):
def _sc2wol_has_nukes(self, multiworld: MultiWorld, player: int) -> bool:
return get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
+ def _sc2wol_can_respond_to_colony_infestations(self, multiworld: MultiWorld, player: int) -> bool:
+ return self._sc2wol_has_common_unit(multiworld, player) \
+ and self._sc2wol_has_competent_anti_air(multiworld, player) \
+ and \
+ (
+ self._sc2wol_has_air_anti_air(multiworld, player) or
+ self.has_any({'Battlecruiser', 'Valkyrie'}), player
+ ) \
+ and \
+ self._sc2wol_defense_rating(multiworld, player, True) >= 3
+
def _sc2wol_final_mission_requirements(self, multiworld: MultiWorld, player: int):
beats_kerrigan = self.has_any({'Marine', 'Banshee', 'Ghost'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0
if get_option_value(multiworld, player, 'all_in_map') == 0:
@@ -101,7 +141,7 @@ class SC2WoLLogic(LogicMixin):
# Air
defense_rating = self._sc2wol_defense_rating(multiworld, player, True, True)
return defense_rating >= 8 and beats_kerrigan \
- and self.has_any({'Viking', 'Battlecruiser'}, player) \
+ and self.has_any({'Viking', 'Battlecruiser', 'Valkyrie'}, player) \
and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player)
def _sc2wol_cleared_missions(self, multiworld: MultiWorld, player: int, mission_count: int) -> bool:
diff --git a/worlds/sc2wol/MissionTables.py b/worlds/sc2wol/MissionTables.py
index 6db93547..298cd7a9 100644
--- a/worlds/sc2wol/MissionTables.py
+++ b/worlds/sc2wol/MissionTables.py
@@ -49,8 +49,8 @@ vanilla_shuffle_order = [
FillMission(MissionPools.EASY, [2], "Artifact", completion_critical=True),
FillMission(MissionPools.MEDIUM, [7], "Artifact", number=8, completion_critical=True),
FillMission(MissionPools.HARD, [8], "Artifact", number=11, completion_critical=True),
- FillMission(MissionPools.HARD, [9], "Artifact", number=14, completion_critical=True),
- FillMission(MissionPools.HARD, [10], "Artifact", completion_critical=True),
+ FillMission(MissionPools.HARD, [9], "Artifact", number=14, completion_critical=True, removal_priority=11),
+ FillMission(MissionPools.HARD, [10], "Artifact", completion_critical=True, removal_priority=10),
FillMission(MissionPools.MEDIUM, [2], "Covert", number=4),
FillMission(MissionPools.MEDIUM, [12], "Covert"),
FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=3),
@@ -58,7 +58,7 @@ vanilla_shuffle_order = [
FillMission(MissionPools.MEDIUM, [2], "Rebellion", number=6),
FillMission(MissionPools.HARD, [16], "Rebellion"),
FillMission(MissionPools.HARD, [17], "Rebellion"),
- FillMission(MissionPools.HARD, [18], "Rebellion"),
+ FillMission(MissionPools.HARD, [18], "Rebellion", removal_priority=12),
FillMission(MissionPools.HARD, [19], "Rebellion", removal_priority=5),
FillMission(MissionPools.MEDIUM, [8], "Prophecy", removal_priority=9),
FillMission(MissionPools.HARD, [21], "Prophecy", removal_priority=8),
@@ -98,6 +98,13 @@ gauntlet_order = [
FillMission(MissionPools.FINAL, [5], "Final", completion_critical=True)
]
+mini_gauntlet_order = [
+ FillMission(MissionPools.STARTER, [-1], "I", completion_critical=True),
+ FillMission(MissionPools.EASY, [0], "II", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [1], "III", completion_critical=True),
+ FillMission(MissionPools.FINAL, [2], "Final", completion_critical=True)
+]
+
grid_order = [
FillMission(MissionPools.STARTER, [-1], "_1"),
FillMission(MissionPools.EASY, [0], "_1"),
@@ -129,6 +136,13 @@ mini_grid_order = [
FillMission(MissionPools.FINAL, [5, 7], "_3", or_requirements=True)
]
+tiny_grid_order = [
+ FillMission(MissionPools.STARTER, [-1], "_1"),
+ FillMission(MissionPools.MEDIUM, [0], "_1"),
+ FillMission(MissionPools.EASY, [0], "_2"),
+ FillMission(MissionPools.FINAL, [1, 2], "_2", or_requirements=True),
+]
+
blitz_order = [
FillMission(MissionPools.STARTER, [-1], "I"),
FillMission(MissionPools.EASY, [-1], "I"),
@@ -144,7 +158,17 @@ blitz_order = [
FillMission(MissionPools.FINAL, [0, 1], "Final", number=5, or_requirements=True)
]
-mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_campaign_order, grid_order, mini_grid_order, blitz_order, gauntlet_order]
+mission_orders = [
+ vanilla_shuffle_order,
+ vanilla_shuffle_order,
+ mini_campaign_order,
+ grid_order,
+ mini_grid_order,
+ blitz_order,
+ gauntlet_order,
+ mini_gauntlet_order,
+ tiny_grid_order
+]
vanilla_mission_req_table = {
@@ -190,7 +214,7 @@ starting_mission_locations = {
"Whispers of Doom": "Whispers of Doom: Victory",
"Belly of the Beast": "Belly of the Beast: Victory",
"Zero Hour": "Zero Hour: First Group Rescued",
- "Evacuation": "Evacuation: First Chysalis",
+ "Evacuation": "Evacuation: Reach Hanson",
"Devil's Playground": "Devil's Playground: Tosh's Miners",
"Smash and Grab": "Smash and Grab: First Relic",
"The Great Train Robbery": "The Great Train Robbery: North Defiler"
diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py
index 4f2032d6..0702e431 100644
--- a/worlds/sc2wol/Options.py
+++ b/worlds/sc2wol/Options.py
@@ -3,31 +3,49 @@ from BaseClasses import MultiWorld
from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range
from .MissionTables import vanilla_mission_req_table
+ORDER_VANILLA = 0
+ORDER_VANILLA_SHUFFLED = 1
class GameDifficulty(Choice):
- """The difficulty of the campaign, affects enemy AI, starting units, and game speed."""
+ """
+ The difficulty of the campaign, affects enemy AI, starting units, and game speed.
+
+ For those unfamiliar with the Archipelago randomizer, the recommended settings are one difficulty level
+ lower than the vanilla game
+ """
display_name = "Game Difficulty"
option_casual = 0
option_normal = 1
option_hard = 2
option_brutal = 3
+ default = 1
+class GameSpeed(Choice):
+ """Optional setting to override difficulty-based game speed."""
+ display_name = "Game Speed"
+ option_default = 0
+ option_slower = 1
+ option_slow = 2
+ option_normal = 3
+ option_fast = 4
+ option_faster = 5
+ default = option_default
-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 FinalMap(Choice):
+ """
+ Determines if the final map and goal of the campaign.
+ All in: You need to beat All-in map
+ Random Hard: A random hard mission is selected as a goal.
+ Beat this mission in order to complete the game.
+ All-in map won't be in the campaign
+ Vanilla mission order always ends with All in mission!
-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
-
+ This option is short-lived. It may be changed in the future
+ """
+ display_name = "Final Map"
+ option_all_in = 0
+ option_random_hard = 1
class AllInMap(Choice):
"""Determines what version of All-In (final map) that will be generated for the campaign."""
@@ -37,14 +55,18 @@ class AllInMap(Choice):
class MissionOrder(Choice):
- """Determines the order the missions are played in. The last three mission orders end in a random mission.
+ """
+ Determines the order the missions are played in. The last three mission orders end in a random mission.
Vanilla (29): Keeps the standard mission order and branching from the WoL Campaign.
Vanilla Shuffled (29): Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.
Mini Campaign (15): Shorter version of the campaign with randomized missions and optional branches.
- Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards All-In.
+ Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards bottom-right mission to win.
Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win.
Blitz (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win.
- Gauntlet (7): Linear series of 7 random missions to complete the campaign."""
+ Gauntlet (7): Linear series of 7 random missions to complete the campaign.
+ Mini Gauntlet (4): Linear series of 4 random missions to complete the campaign.
+ Tiny Grid (4): A 2x2 version of Grid. Complete the bottom-right mission to win.
+ """
display_name = "Mission Order"
option_vanilla = 0
option_vanilla_shuffled = 1
@@ -53,27 +75,53 @@ class MissionOrder(Choice):
option_mini_grid = 4
option_blitz = 5
option_gauntlet = 6
+ option_mini_gauntlet = 7
+ option_tiny_grid = 8
+
+
+class PlayerColor(Choice):
+ """Determines in-game team color."""
+ display_name = "Player Color"
+ option_white = 0
+ option_red = 1
+ option_blue = 2
+ option_teal = 3
+ option_purple = 4
+ option_yellow = 5
+ option_orange = 6
+ option_green = 7
+ option_light_pink = 8
+ option_violet = 9
+ option_light_grey = 10
+ option_dark_green = 11
+ option_brown = 12
+ option_light_green = 13
+ option_dark_grey = 14
+ option_pink = 15
+ option_rainbow = 16
+ option_default = 17
+ default = option_default
class ShuffleProtoss(DefaultOnToggle):
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled.
- If turned off with Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain
- if not shuffled.
- If turned off with reduced mission settings, the 3 protoss missions will not appear and Protoss units are removed
- from the pool."""
+ If turned off, the 3 protoss missions will not appear and Protoss units are removed from the pool."""
display_name = "Shuffle Protoss Missions"
class ShuffleNoBuild(DefaultOnToggle):
"""Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
- If turned off with Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be
- placed at the end of optional routes.
- If turned off with reduced mission settings, the 5 no-build missions will not appear."""
+ If turned off, the 5 no-build missions will not appear."""
display_name = "Shuffle No-Build Missions"
class EarlyUnit(DefaultOnToggle):
- """Guarantees that the first mission will contain a unit."""
+ """
+ Guarantees that the first mission will contain a unit.
+
+ Each mission available to be the first mission has a pre-defined location where the unit should spawn.
+ This location gets overriden over any exclusion. It's guaranteed to be reachable with an empty inventory.
+ """
display_name = "Early Unit"
@@ -91,11 +139,97 @@ class RequiredTactics(Choice):
class UnitsAlwaysHaveUpgrades(DefaultOnToggle):
- """If turned on, both upgrades will be present for each unit and structure in the seed.
- This usually results in fewer units."""
+ """
+ If turned on, all upgrades will be present for each unit and structure in the seed.
+ This usually results in fewer units.
+
+ See also: Max Number of Upgrades
+ """
display_name = "Units Always Have Upgrades"
+class GenericUpgradeMissions(Range):
+ """Determines the percentage of missions in the mission order that must be completed before
+ level 1 of all weapon and armor upgrades is unlocked. Level 2 upgrades require double the amount of missions,
+ and level 3 requires triple the amount. The required amounts are always rounded down.
+ If set to 0, upgrades are instead added to the item pool and must be found to be used."""
+ display_name = "Generic Upgrade Missions"
+ range_start = 0
+ range_end = 100
+ default = 0
+
+
+class GenericUpgradeResearch(Choice):
+ """Determines how weapon and armor upgrades affect missions once unlocked.
+
+ Vanilla: Upgrades must be researched as normal.
+ Auto In No-Build: In No-Build missions, upgrades are automatically researched.
+ In all other missions, upgrades must be researched as normal.
+ Auto In Build: In No-Build missions, upgrades are unavailable as normal.
+ In all other missions, upgrades are automatically researched.
+ Always Auto: Upgrades are automatically researched in all missions."""
+ display_name = "Generic Upgrade Research"
+ option_vanilla = 0
+ option_auto_in_no_build = 1
+ option_auto_in_build = 2
+ option_always_auto = 3
+
+
+class GenericUpgradeItems(Choice):
+ """Determines how weapon and armor upgrades are split into items. All options produce 3 levels of each item.
+ Does nothing if upgrades are unlocked by completed mission counts.
+
+ Individual Items: All weapon and armor upgrades are each an item,
+ resulting in 18 total upgrade items.
+ Bundle Weapon And Armor: All types of weapon upgrades are one item,
+ and all types of armor upgrades are one item,
+ resulting in 6 total items.
+ Bundle Unit Class: Weapon and armor upgrades are merged,
+ but Infantry, Vehicle, and Starship upgrades are bundled separately,
+ resulting in 9 total items.
+ Bundle All: All weapon and armor upgrades are one item,
+ resulting in 3 total items."""
+ display_name = "Generic Upgrade Items"
+ option_individual_items = 0
+ option_bundle_weapon_and_armor = 1
+ option_bundle_unit_class = 2
+ option_bundle_all = 3
+
+
+class NovaCovertOpsItems(Toggle):
+ """If turned on, the equipment upgrades from Nova Covert Ops may be present in the world."""
+ display_name = "Nova Covert Ops Items"
+ default = Toggle.option_true
+
+
+class BroodWarItems(Toggle):
+ """If turned on, returning items from StarCraft: Brood War may appear in the world."""
+ display_name = "Brood War Items"
+ default = Toggle.option_true
+
+
+class ExtendedItems(Toggle):
+ """If turned on, original items that did not appear in Campaign mode may appear in the world."""
+ display_name = "Extended Items"
+ default = Toggle.option_true
+
+
+class MaxNumberOfUpgrades(Range):
+ """
+ Set a maximum to the number of upgrades a unit/structure can have. -1 is used to define unlimited.
+ Note that most unit have 4 or 6 upgrades.
+
+ If used with Units Always Have Upgrades, each unit has this given amount of upgrades (if there enough upgrades exist)
+
+ See also: Units Always Have Upgrades
+ """
+ display_name = "Maximum number of upgrades per unit/structure"
+ range_start = -1
+ # Do not know the maximum, but it is less than 123!
+ range_end = 123
+ default = -1
+
+
class LockedItems(ItemSet):
"""Guarantees that these items will be unlockable"""
display_name = "Locked Items"
@@ -108,27 +242,114 @@ class ExcludedItems(ItemSet):
class ExcludedMissions(OptionSet):
"""Guarantees that these missions will not appear in the campaign
- Only applies on shortened mission orders.
+ Doesn't apply to vanilla mission order.
It may be impossible to build a valid campaign if too many missions are excluded."""
display_name = "Excluded Missions"
valid_keys = {mission_name for mission_name in vanilla_mission_req_table.keys() if mission_name != 'All-In'}
+class LocationInclusion(Choice):
+ option_enabled = 0
+ option_trash = 1
+ option_nothing = 2
+
+
+class MissionProgressLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for progressing (not finishing) a mission.
+ Progressing a mission is usually a task of completing or progressing into a main objective.
+ Clearing an expansion base also counts here.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Trash: Forces a trash item in
+ Nothing: No rewards for this type of tasks, effectively disabling such locations
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ Warning: The generation may fail if too many locations are excluded by this way.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Mission Progress Locations"
+
+
+class BonusLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for completing bonus tasks.
+ Bonus tasks are those giving you a campaign-wide or mission-wide bonus in vanilla game:
+ Research, credits, bonus units or resources, etc.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Trash: Forces a trash item in
+ Nothing: No rewards for this type of tasks, effectively disabling such locations
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ Warning: The generation may fail if too many locations are excluded by this way.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Bonus Locations"
+
+
+class ChallengeLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for completing challenge tasks.
+ Challenges are tasks that have usually higher requirements to be completed
+ than to complete the mission they're in successfully.
+ You might be required to visit the same mission later when getting stronger in order to finish these tasks.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Trash: Forces a trash item in
+ Nothing: No rewards for this type of tasks, effectively disabling such locations
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ Warning: The generation may fail if too many locations are excluded by this way.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Challenge Locations"
+
+
+class OptionalBossLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for defeating optional bosses.
+ An optional boss is any boss that's not required to kill in order to finish the mission successfully.
+ All Brutalisks, Loki, etc. belongs here.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Trash: Forces a trash item in
+ Nothing: No rewards for this type of tasks, effectively disabling such locations
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ Warning: The generation may fail if too many locations are excluded by this way.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Challenge Locations"
+
+
# noinspection PyTypeChecker
sc2wol_options: Dict[str, Option] = {
"game_difficulty": GameDifficulty,
- "upgrade_bonus": UpgradeBonus,
- "bunker_upgrade": BunkerUpgrade,
+ "game_speed": GameSpeed,
"all_in_map": AllInMap,
+ "final_map": FinalMap,
"mission_order": MissionOrder,
+ "player_color": PlayerColor,
"shuffle_protoss": ShuffleProtoss,
"shuffle_no_build": ShuffleNoBuild,
"early_unit": EarlyUnit,
"required_tactics": RequiredTactics,
"units_always_have_upgrades": UnitsAlwaysHaveUpgrades,
+ "max_number_of_upgrades": MaxNumberOfUpgrades,
+ "generic_upgrade_missions": GenericUpgradeMissions,
+ "generic_upgrade_research": GenericUpgradeResearch,
+ "generic_upgrade_items": GenericUpgradeItems,
"locked_items": LockedItems,
"excluded_items": ExcludedItems,
- "excluded_missions": ExcludedMissions
+ "excluded_missions": ExcludedMissions,
+ "nco_items": NovaCovertOpsItems,
+ "bw_items": BroodWarItems,
+ "ext_items": ExtendedItems,
+ "mission_progress_locations": MissionProgressLocations,
+ "bonus_locations": BonusLocations,
+ "challenge_locations": ChallengeLocations,
+ "optional_boss_locations": OptionalBossLocations
}
diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py
index 16cc51f2..4a19e2db 100644
--- a/worlds/sc2wol/PoolFilter.py
+++ b/worlds/sc2wol/PoolFilter.py
@@ -1,22 +1,22 @@
from typing import Callable, Dict, List, Set
from BaseClasses import MultiWorld, ItemClassification, Item, Location
-from .Items import item_table
+from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items
from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\
mission_orders, MissionInfo, alt_final_mission_locations, MissionPools
-from .Options import get_option_value
+from .Options import get_option_value, MissionOrder, FinalMap, MissionProgressLocations, LocationInclusion
from .LogicMixin import SC2WoLLogic
# Items with associated upgrades
UPGRADABLE_ITEMS = [
"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre",
- "Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor",
- "Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser",
+ "Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine", "Cyclone",
+ "Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Raven", "Science Vessel", "Liberator", "Valkyrie",
"Bunker", "Missile Turret"
]
BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"}
-FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator"}
-STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven"}
+FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine"}
+STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven", "Liberator", "Valkyrie"}
PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"}
@@ -30,7 +30,7 @@ def filter_missions(multiworld: MultiWorld, player: int) -> Dict[int, List[str]]
shuffle_no_build = get_option_value(multiworld, player, "shuffle_no_build")
shuffle_protoss = get_option_value(multiworld, player, "shuffle_protoss")
excluded_missions = get_option_value(multiworld, player, "excluded_missions")
- mission_count = len(mission_orders[mission_order_type]) - 1
+ final_map = get_option_value(multiworld, player, "final_map")
mission_pools = {
MissionPools.STARTER: no_build_regions_list[:],
MissionPools.EASY: easy_regions_list[:],
@@ -38,21 +38,18 @@ def filter_missions(multiworld: MultiWorld, player: int) -> Dict[int, List[str]]
MissionPools.HARD: hard_regions_list[:],
MissionPools.FINAL: []
}
- if mission_order_type == 0:
+ if mission_order_type == MissionOrder.option_vanilla:
# Vanilla uses the entire mission pool
mission_pools[MissionPools.FINAL] = ['All-In']
return mission_pools
- elif mission_order_type == 1:
- # Vanilla Shuffled ignores the player-provided excluded missions
- excluded_missions = set()
# Omitting No-Build missions if not shuffling no-build
if not shuffle_no_build:
excluded_missions = excluded_missions.union(no_build_regions_list)
# Omitting Protoss missions if not shuffling protoss
if not shuffle_protoss:
excluded_missions = excluded_missions.union(PROTOSS_REGIONS)
- # Replacing All-In on low mission counts
- if mission_count < 14:
+ # Replacing All-In with alternate ending depending on option
+ if final_map == FinalMap.option_random_hard:
final_mission = multiworld.random.choice([mission for mission in alt_final_mission_locations.keys() if mission not in excluded_missions])
excluded_missions.add(final_mission)
else:
@@ -92,10 +89,18 @@ def get_item_upgrades(inventory: List[Item], parent_item: Item or str):
item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
return [
inv_item for inv_item in inventory
- if item_table[inv_item.name].parent_item == item_name
+ if get_full_item_list()[inv_item.name].parent_item == item_name
]
+def get_item_quantity(item):
+ return get_full_item_list()[item.name].quantity
+
+
+def copy_item(item: Item):
+ return Item(item.name, item.classification, item.code, item.player)
+
+
class ValidInventory:
def has(self, item: str, player: int):
@@ -124,22 +129,6 @@ class ValidInventory:
cascade_keys = self.cascade_removal_map.keys()
units_always_have_upgrades = get_option_value(self.multiworld, self.player, "units_always_have_upgrades")
- # Locking associated items for items that have already been placed when units_always_have_upgrades is on
- if units_always_have_upgrades:
- existing_items = self.existing_items[:]
- while existing_items:
- existing_item = existing_items.pop()
- items_to_lock = self.cascade_removal_map.get(existing_item, [existing_item])
- for item in items_to_lock:
- if item in inventory:
- inventory.remove(item)
- locked_items.append(item)
- if item in existing_items:
- existing_items.remove(item)
-
- if self.min_units_per_structure > 0 and self.has_units_per_structure():
- requirements.append(lambda state: state.has_units_per_structure())
-
def attempt_removal(item: Item) -> bool:
# If item can be removed and has associated items, remove them as well
inventory.remove(item)
@@ -149,9 +138,77 @@ class ValidInventory:
if not all(requirement(self) for requirement in requirements):
# If item cannot be removed, lock or revert
self.logical_inventory.add(item.name)
- locked_items.append(item)
+ for _ in range(get_item_quantity(item)):
+ locked_items.append(copy_item(item))
return False
return True
+
+ # Limit the maximum number of upgrades
+ maxUpgrad = get_option_value(self.multiworld, self.player,
+ "max_number_of_upgrades")
+ if maxUpgrad != -1:
+ unit_avail_upgrades = {}
+ # Needed to take into account locked/existing items
+ unit_nb_upgrades = {}
+ for item in inventory:
+ cItem = get_full_item_list()[item.name]
+ if cItem.type in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
+ unit_avail_upgrades[item.name] = []
+ unit_nb_upgrades[item.name] = 0
+ elif cItem.parent_item is not None:
+ if cItem.parent_item not in unit_avail_upgrades:
+ unit_avail_upgrades[cItem.parent_item] = [item]
+ unit_nb_upgrades[cItem.parent_item] = 1
+ else:
+ unit_avail_upgrades[cItem.parent_item].append(item)
+ unit_nb_upgrades[cItem.parent_item] += 1
+ # For those two categories, we count them but dont include them in removal
+ for item in locked_items + self.existing_items:
+ cItem = get_full_item_list()[item.name]
+ if cItem.type in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
+ unit_avail_upgrades[item.name] = []
+ unit_nb_upgrades[item.name] = 0
+ elif cItem.parent_item is not None:
+ if cItem.parent_item not in unit_avail_upgrades:
+ unit_nb_upgrades[cItem.parent_item] = 1
+ else:
+ unit_nb_upgrades[cItem.parent_item] += 1
+ # Making sure that the upgrades being removed is random
+ # Currently, only for combat shield vs Stabilizer Medpacks...
+ shuffled_unit_upgrade_list = list(unit_avail_upgrades.keys())
+ self.multiworld.random.shuffle(shuffled_unit_upgrade_list)
+ for unit in shuffled_unit_upgrade_list:
+ while (unit_nb_upgrades[unit] > maxUpgrad) \
+ and (len(unit_avail_upgrades[unit]) > 0):
+ itemCandidate = self.multiworld.random.choice(unit_avail_upgrades[unit])
+ _ = attempt_removal(itemCandidate)
+ # Whatever it succeed to remove the iventory or it fails and thus
+ # lock it, the upgrade is no longer available for removal
+ unit_avail_upgrades[unit].remove(itemCandidate)
+ unit_nb_upgrades[unit] -= 1
+
+ # Locking associated items for items that have already been placed when units_always_have_upgrades is on
+ if units_always_have_upgrades:
+ existing_items = set(self.existing_items[:] + locked_items)
+ while existing_items:
+ existing_item = existing_items.pop()
+ items_to_lock = self.cascade_removal_map.get(existing_item, [existing_item])
+ if get_full_item_list()[existing_item.name].type != "Upgrade":
+ # Don't process general upgrades, they may have been pre-locked per-level
+ for item in items_to_lock:
+ if item in inventory:
+ # Unit upgrades, lock all levels
+ for _ in range(inventory.count(item)):
+ inventory.remove(item)
+ if item not in locked_items:
+ # Lock all the associated items if not already locked
+ for _ in range(get_item_quantity(item)):
+ locked_items.append(copy_item(item))
+ if item in existing_items:
+ existing_items.remove(item)
+
+ if self.min_units_per_structure > 0 and self.has_units_per_structure():
+ requirements.append(lambda state: state.has_units_per_structure())
# Determining if the full-size inventory can complete campaign
if not all(requirement(self) for requirement in requirements):
@@ -185,21 +242,47 @@ class ValidInventory:
if cascade_failure:
for transient_item in transient_items:
if transient_item in inventory:
- inventory.remove(transient_item)
+ for _ in range(inventory.count(transient_item)):
+ inventory.remove(transient_item)
if transient_item not in locked_items:
- locked_items.append(transient_item)
+ for _ in range(get_item_quantity(transient_item)):
+ locked_items.append(copy_item(transient_item))
if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing):
self.logical_inventory.add(transient_item.name)
else:
attempt_removal(item)
- return inventory + locked_items
+ if not spider_mine_sources & self.logical_inventory:
+ inventory = [item for item in inventory if not item.name.endswith("(Spider Mine)")]
+ if not BARRACKS_UNITS & self.logical_inventory:
+ inventory = [item for item in inventory if
+ not (item.name.startswith("Progressive Infantry") or item.name == "Orbital Strike")]
+ if not FACTORY_UNITS & self.logical_inventory:
+ inventory = [item for item in inventory if not item.name.startswith("Progressive Vehicle")]
+ if not STARPORT_UNITS & self.logical_inventory:
+ inventory = [item for item in inventory if not item.name.startswith("Progressive Ship")]
+
+ # Cull finished, adding locked items back into inventory
+ inventory += locked_items
+
+ # Replacing empty space with generically useful items
+ replacement_items = [item for item in self.item_pool
+ if (item not in inventory
+ and item not in self.locked_items
+ and item.name in second_pass_placeable_items)]
+ self.multiworld.random.shuffle(replacement_items)
+ while len(inventory) < inventory_size and len(replacement_items) > 0:
+ item = replacement_items.pop()
+ inventory.append(item)
+
+ return inventory
def _read_logic(self):
self._sc2wol_has_common_unit = lambda world, player: SC2WoLLogic._sc2wol_has_common_unit(self, world, player)
self._sc2wol_has_air = lambda world, player: SC2WoLLogic._sc2wol_has_air(self, world, player)
self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player)
self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player)
+ self._sc2wol_has_competent_ground_to_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_ground_to_air(self, world, player)
self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player)
self._sc2wol_defense_rating = lambda world, player, zerg_enemy, air_enemy=False: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy, air_enemy)
self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player)
@@ -210,6 +293,8 @@ class ValidInventory:
self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player)
self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player)
self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player)
+ self._sc2wol_welcome_to_the_jungle_requirement = lambda world, player: SC2WoLLogic._sc2wol_welcome_to_the_jungle_requirement(self, world, player)
+ self._sc2wol_can_respond_to_colony_infestations = lambda world, player: SC2WoLLogic._sc2wol_can_respond_to_colony_infestations(self, world, player)
self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player)
def __init__(self, multiworld: MultiWorld, player: int,
@@ -230,7 +315,7 @@ class ValidInventory:
self.min_units_per_structure = int(mission_count / 7)
min_upgrades = 1 if mission_count < 10 else 2
for item in item_pool:
- item_info = item_table[item.name]
+ item_info = get_full_item_list()[item.name]
if item_info.type == "Upgrade":
# Locking upgrades based on mission duration
if item.name not in item_quantities:
diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py
index 03363666..f588ce7e 100644
--- a/worlds/sc2wol/Regions.py
+++ b/worlds/sc2wol/Regions.py
@@ -1,10 +1,14 @@
from typing import List, Set, Dict, Tuple, Optional, Callable
from BaseClasses import MultiWorld, Region, Entrance, Location
from .Locations import LocationData
-from .Options import get_option_value
-from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, MissionPools
+from .Options import get_option_value, MissionOrder
+from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, \
+ MissionPools, vanilla_shuffle_order
from .PoolFilter import filter_missions
+PROPHECY_CHAIN_MISSION_COUNT = 4
+
+VANILLA_SHUFFLED_FIRST_PROPHECY_MISSION = 21
def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\
-> Tuple[Dict[str, MissionInfo], int, str]:
@@ -19,7 +23,7 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio
names: Dict[str, int] = {}
- if mission_order_type == 0:
+ if mission_order_type == MissionOrder.option_vanilla:
# Generating all regions and locations
for region_name in vanilla_mission_req_table.keys():
@@ -108,12 +112,17 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio
removals = len(mission_order) - mission_pool_size
# Removing entire Prophecy chain on vanilla shuffled when not shuffling protoss
if remove_prophecy:
- removals -= 4
+ removals -= PROPHECY_CHAIN_MISSION_COUNT
# Initial fill out of mission list and marking all-in mission
for mission in mission_order:
# Removing extra missions if mission pool is too small
- if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy:
+ # Also handle lower removal priority than Prophecy
+ if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy \
+ or (remove_prophecy and mission_order_type == MissionOrder.option_vanilla_shuffled
+ and mission.removal_priority > vanilla_shuffle_order[
+ VANILLA_SHUFFLED_FIRST_PROPHECY_MISSION].removal_priority
+ and 0 < mission.removal_priority <= removals + PROPHECY_CHAIN_MISSION_COUNT):
missions.append(None)
elif mission.type == MissionPools.FINAL:
missions.append(final_mission)
@@ -191,22 +200,38 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio
# TODO: Handle 'and' connections
mission_req_table = {}
+ def build_connection_rule(mission_names: List[str], missions_req: int) -> Callable:
+ if len(mission_names) > 1:
+ return lambda state: state.has_all({f"Beat {name}" for name in mission_names}, player) and \
+ state._sc2wol_cleared_missions(multiworld, player, missions_req)
+ else:
+ return lambda state: state.has(f"Beat {mission_names[0]}", player) and \
+ state._sc2wol_cleared_missions(multiworld, player, missions_req)
+
for i, mission in enumerate(missions):
if mission is None:
continue
connections = []
+ all_connections = []
+ for connection in mission_order[i].connect_to:
+ if connection == -1:
+ continue
+ while missions[connection] is None:
+ connection -= 1
+ all_connections.append(missions[connection])
for connection in mission_order[i].connect_to:
required_mission = missions[connection]
if connection == -1:
connect(multiworld, player, names, "Menu", mission)
- elif required_mission is None:
- continue
else:
+ if required_mission is None and not mission_order[i].completion_critical: # Drop non-critical null slots
+ continue
+ while required_mission is None: # Substituting null slot with prior slot
+ connection -= 1
+ required_mission = missions[connection]
+ required_missions = [required_mission] if mission_order[i].or_requirements else all_connections
connect(multiworld, player, names, required_mission, mission,
- (lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and
- state._sc2wol_cleared_missions(multiworld, player,
- missions_req)))
- (missions[connection], mission_order[i].number))
+ build_connection_rule(required_missions, mission_order[i].number))
connections.append(slot_map[connection])
mission_req_table.update({mission: MissionInfo(
diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py
index 49052429..93aebb7a 100644
--- a/worlds/sc2wol/__init__.py
+++ b/worlds/sc2wol/__init__.py
@@ -3,11 +3,11 @@ 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, item_table, filler_items, item_name_groups, get_full_item_list, \
- get_basic_units
-from .Locations import get_locations
+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
+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
@@ -36,7 +36,7 @@ class SC2WoLWorld(World):
web = Starcraft2WoLWebWorld()
data_version = 4
- item_name_to_id = {name: data.code for name, data in item_table.items()}
+ 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
@@ -69,6 +69,8 @@ class SC2WoLWorld(World):
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)
@@ -109,16 +111,6 @@ def setup_events(player: int, locked_locations: typing.List[str], location_cache
def get_excluded_items(multiworld: MultiWorld, player: int) -> Set[str]:
excluded_items: Set[str] = set()
- if get_option_value(multiworld, player, "upgrade_bonus") == 1:
- excluded_items.add("Ultra-Capacitors")
- else:
- excluded_items.add("Vanadium Plating")
-
- if get_option_value(multiworld, player, "bunker_upgrade") == 1:
- excluded_items.add("Shrike Turret")
- else:
- excluded_items.add("Fortified Bunker")
-
for item in multiworld.precollected_items[player]:
excluded_items.add(item.name)
@@ -167,7 +159,7 @@ def assign_starter_item(multiworld: MultiWorld, player: int, excluded_items: Set
def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo],
- starter_items: List[str], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
+ 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
@@ -176,17 +168,43 @@ def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[s
# YAML items
yaml_locked_items = get_option_value(multiworld, player, 'locked_items')
- for name, data in item_table.items():
- if name not in excluded_items:
- for _ in range(data.quantity):
- item = create_item_with_correct_settings(player, name)
- if name in yaml_locked_items:
- locked_items.append(item)
- else:
- pool.append(item)
+ # 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:
@@ -207,8 +225,100 @@ def fill_item_pool_with_dummy_items(self: SC2WoLWorld, multiworld: MultiWorld, p
def create_item_with_correct_settings(player: int, name: str) -> Item:
- data = item_table[name]
+ 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