SC2: Option for random mission order (#569)

This commit is contained in:
TheCondor07 2022-05-26 13:28:10 -04:00 committed by GitHub
parent cec0e2cbfb
commit e786243738
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 422 additions and 140 deletions

View File

@ -11,6 +11,8 @@ from sc2.main import run_game
from sc2.data import Race from sc2.data import Race
from sc2.bot_ai import BotAI from sc2.bot_ai import BotAI
from sc2.player import Bot from sc2.player import Bot
from worlds.sc2wol.Regions import MissionInfo
from worlds.sc2wol.MissionTables import lookup_id_to_mission
from worlds.sc2wol.Items import lookup_id_to_name, item_table from worlds.sc2wol.Items import lookup_id_to_name, item_table
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
@ -32,6 +34,13 @@ nest_asyncio.apply()
class StarcraftClientProcessor(ClientCommandProcessor): class StarcraftClientProcessor(ClientCommandProcessor):
ctx: Context ctx: Context
missions_unlocked = 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.missions_unlocked = True
sc2_logger.info("Mission check has been disabled")
def _cmd_play(self, mission_id: str = "") -> bool: def _cmd_play(self, mission_id: str = "") -> bool:
"""Start a Starcraft 2 mission""" """Start a Starcraft 2 mission"""
@ -42,7 +51,8 @@ class StarcraftClientProcessor(ClientCommandProcessor):
if num_options > 0: if num_options > 0:
mission_number = int(options[0]) mission_number = int(options[0])
if is_mission_available(mission_number, self.ctx.checked_locations, mission_req_table): if self.missions_unlocked or \
is_mission_available(mission_number, self.ctx.checked_locations, self.ctx.mission_req_table):
if self.ctx.sc2_run_task: if self.ctx.sc2_run_task:
if not self.ctx.sc2_run_task.done(): if not self.ctx.sc2_run_task.done():
sc2_logger.warning("Starcraft 2 Client is still running!") sc2_logger.warning("Starcraft 2 Client is still running!")
@ -65,13 +75,13 @@ class StarcraftClientProcessor(ClientCommandProcessor):
def _cmd_available(self) -> bool: def _cmd_available(self) -> bool:
"""Get what missions are currently available to play""" """Get what missions are currently available to play"""
request_available_missions(self.ctx.checked_locations, mission_req_table, self.ctx.ui) request_available_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui)
return True return True
def _cmd_unfinished(self) -> bool: def _cmd_unfinished(self) -> bool:
"""Get what missions are currently available to play and have not had all locations checked""" """Get what missions are currently available to play and have not had all locations checked"""
request_unfinished_missions(self.ctx.checked_locations, mission_req_table, self.ctx.ui) request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx)
return True return True
@ -81,6 +91,7 @@ class Context(CommonContext):
items_handling = 0b111 items_handling = 0b111
difficulty = -1 difficulty = -1
all_in_choice = 0 all_in_choice = 0
mission_req_table = None
items_rec_to_announce = [] items_rec_to_announce = []
rec_announce_pos = 0 rec_announce_pos = 0
items_sent_to_announce = [] items_sent_to_announce = []
@ -102,6 +113,11 @@ class Context(CommonContext):
if cmd in {"Connected"}: if cmd in {"Connected"}:
self.difficulty = args["slot_data"]["game_difficulty"] self.difficulty = args["slot_data"]["game_difficulty"]
self.all_in_choice = args["slot_data"]["all_in_map"] self.all_in_choice = args["slot_data"]["all_in_map"]
slot_req_table = args["slot_data"]["mission_req"]
self.mission_req_table = {}
for mission in slot_req_table:
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
if cmd in {"PrintJSON"}: if cmd in {"PrintJSON"}:
noted = False noted = False
if "receiving" in args: if "receiving" in args:
@ -224,8 +240,8 @@ async def starcraft_launch(ctx: Context, mission_id):
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.") sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
run_game(sc2.maps.get(maps_table[mission_id - 1]), [ run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), name="Archipelago", fullscreen=True)], realtime=True) name="Archipelago", fullscreen=True)], realtime=True)
class ArchipelagoBot(sc2.bot_ai.BotAI): class ArchipelagoBot(sc2.bot_ai.BotAI):
@ -302,7 +318,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
game_state = int(38281 - unit.health) game_state = int(38281 - unit.health)
self.can_read_game = True self.can_read_game = True
if iteration == 80 and not game_state & 1: if iteration == 160 and not game_state & 1:
await self.chat_send("SendMessage Warning: Archipelago unable to connect or has lost connection to " + await self.chat_send("SendMessage Warning: Archipelago unable to connect or has lost connection to " +
"Starcraft 2 (This is likely a map issue)") "Starcraft 2 (This is likely a map issue)")
@ -390,15 +406,6 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
await self.chat_send("LostConnection - Lost connection to game.") await self.chat_send("LostConnection - Lost connection to game.")
class MissionInfo(typing.NamedTuple):
id: int
extra_locations: int
required_world: list[int]
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
mission_req_table = { mission_req_table = {
"Liberation Day": MissionInfo(1, 7, [], completion_critical=True), "Liberation Day": MissionInfo(1, 7, [], completion_critical=True),
"The Outlaws": MissionInfo(2, 2, [1], completion_critical=True), "The Outlaws": MissionInfo(2, 2, [1], completion_critical=True),
@ -431,17 +438,17 @@ mission_req_table = {
"All-In": MissionInfo(29, -1, [27, 28], completion_critical=True, or_requirements=True) "All-In": MissionInfo(29, -1, [27, 28], completion_critical=True, or_requirements=True)
} }
lookup_id_to_mission: typing.Dict[int, str] = {
data.id: mission_name for mission_name, data in mission_req_table.items() if data.id}
def calc_objectives_completed(mission, missions_info, locations_done, unfinished_locations, ctx):
def calc_objectives_completed(mission, missions_info, locations_done):
objectives_complete = 0 objectives_complete = 0
if missions_info[mission].extra_locations > 0: if missions_info[mission].extra_locations > 0:
for i in range(missions_info[mission].extra_locations): for i in range(missions_info[mission].extra_locations):
if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done: if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done:
objectives_complete += 1 objectives_complete += 1
else:
unfinished_locations[mission].append(ctx.location_name_getter(
missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i))
return objectives_complete return objectives_complete
@ -449,31 +456,37 @@ def calc_objectives_completed(mission, missions_info, locations_done):
return -1 return -1
def request_unfinished_missions(locations_done, location_table, ui): def request_unfinished_missions(locations_done, location_table, ui, ctx):
message = "Unfinished Missions: " if location_table:
message = "Unfinished Missions: "
unlocks = initialize_blank_mission_dict(location_table)
unfinished_locations = initialize_blank_mission_dict(location_table)
unfinished_missions = calc_unfinished_missions(locations_done, location_table) unfinished_missions = calc_unfinished_missions(locations_done, location_table, unlocks, unfinished_locations, ctx)
message += ", ".join(f"{mark_up_mission_name(mission, location_table, ui,unlocks)}[{location_table[mission].id}] " +
mark_up_objectives(
f"[{unfinished_missions[mission]}/{location_table[mission].extra_locations}]",
ctx, unfinished_locations, mission)
for mission in unfinished_missions)
message += ", ".join(f"{mark_critical(mission,location_table, ui)}[{location_table[mission].id}] " if ui:
f"({unfinished_missions[mission]}/{location_table[mission].extra_locations})" ui.log_panels['All'].on_message_markup(message)
for mission in unfinished_missions) ui.log_panels['Starcraft2'].on_message_markup(message)
else:
if ui: sc2_logger.info(message)
ui.log_panels['All'].on_message_markup(message)
ui.log_panels['Starcraft2'].on_message_markup(message)
else: else:
sc2_logger.info(message) sc2_logger.warning("No mission table found, you are likely not connected to a server.")
def calc_unfinished_missions(locations_done, locations): def calc_unfinished_missions(locations_done, locations, unlocks, unfinished_locations, ctx):
unfinished_missions = [] unfinished_missions = []
locations_completed = [] locations_completed = []
available_missions = calc_available_missions(locations_done, locations) available_missions = calc_available_missions(locations_done, locations, unlocks)
for name in available_missions: for name in available_missions:
if not locations[name].extra_locations == -1: if not locations[name].extra_locations == -1:
objectives_completed = calc_objectives_completed(name, locations, locations_done) objectives_completed = calc_objectives_completed(name, locations, locations_done, unfinished_locations, ctx)
if objectives_completed < locations[name].extra_locations: if objectives_completed < locations[name].extra_locations:
unfinished_missions.append(name) unfinished_missions.append(name)
@ -492,31 +505,65 @@ def is_mission_available(mission_id_to_check, locations_done, locations):
return any(mission_id_to_check == locations[mission].id for mission in unfinished_missions) return any(mission_id_to_check == locations[mission].id for mission in unfinished_missions)
def mark_critical(mission, location_table, ui): def mark_up_mission_name(mission, location_table, ui, unlock_table):
"""Checks if the mission is required for game completion and adds '*' to the name to mark that.""" """Checks if the mission is required for game completion and adds '*' to the name to mark that."""
if location_table[mission].completion_critical: if location_table[mission].completion_critical:
if ui: if ui:
return "[color=AF99EF]" + mission + "[/color]" message = "[color=AF99EF]" + mission + "[/color]"
else: else:
return "*" + mission + "*" message = "*" + mission + "*"
else: else:
return mission message = mission
if ui:
unlocks = unlock_table[mission]
if len(unlocks) > 0:
pre_message = f"[ref={list(location_table).index(mission)}|Unlocks: "
pre_message += ", ".join(f"{unlock}({location_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 += "<br>".join(location for location in locations)
pre_message += f"]"
formatted_message = pre_message + message + "[/ref]"
return formatted_message
def request_available_missions(locations_done, location_table, ui): def request_available_missions(locations_done, location_table, ui):
message = "Available Missions: " if location_table:
message = "Available Missions: "
missions = calc_available_missions(locations_done, location_table) # Initialize mission unlock table
message += ", ".join(f"{mark_critical(mission,location_table, ui)}[{location_table[mission].id}]" for mission in missions) unlocks = initialize_blank_mission_dict(location_table)
if ui: missions = calc_available_missions(locations_done, location_table, unlocks)
ui.log_panels['All'].on_message_markup(message) message += \
ui.log_panels['Starcraft2'].on_message_markup(message) ", ".join(f"{mark_up_mission_name(mission, location_table, ui, unlocks)}[{location_table[mission].id}]"
for mission in missions)
if ui:
ui.log_panels['All'].on_message_markup(message)
ui.log_panels['Starcraft2'].on_message_markup(message)
else:
sc2_logger.info(message)
else: else:
sc2_logger.info(message) sc2_logger.warning("No mission table found, you are likely not connected to a server.")
def calc_available_missions(locations_done, locations): def calc_available_missions(locations_done, locations, unlocks=None):
available_missions = [] available_missions = []
missions_complete = 0 missions_complete = 0
@ -526,6 +573,11 @@ def calc_available_missions(locations_done, locations):
missions_complete += 1 missions_complete += 1
for name in locations: for name in locations:
# Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
if unlocks:
for unlock in locations[name].required_world:
unlocks[list(locations)[unlock-1]].append(name)
if mission_reqs_completed(name, missions_complete, locations_done, locations): if mission_reqs_completed(name, missions_complete, locations_done, locations):
available_missions.append(name) available_missions.append(name)
@ -549,14 +601,14 @@ def mission_reqs_completed(location_to_check, missions_complete, locations_done,
req_success = True req_success = True
# Check if required mission has been completed # Check if required mission has been completed
if not (req_mission * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done: if not (locations[list(locations)[req_mission-1]].id * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done:
if not locations[location_to_check].or_requirements: if not locations[location_to_check].or_requirements:
return False return False
else: else:
req_success = False req_success = False
# Recursively check required mission to see if it's requirements are met, in case !collect has been done # Recursively check required mission to see if it's requirements are met, in case !collect has been done
if not mission_reqs_completed(lookup_id_to_mission[req_mission], missions_complete, locations_done, if not mission_reqs_completed(list(locations)[req_mission-1], missions_complete, locations_done,
locations): locations):
if not locations[location_to_check].or_requirements: if not locations[location_to_check].or_requirements:
return False return False
@ -581,6 +633,15 @@ def mission_reqs_completed(location_to_check, missions_complete, locations_done,
return True return True
def initialize_blank_mission_dict(location_table):
unlocks = {}
for mission in list(location_table):
unlocks[mission] = []
return unlocks
if __name__ == '__main__': if __name__ == '__main__':
colorama.init() colorama.init()
asyncio.run(main()) asyncio.run(main())

View File

@ -100,7 +100,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
lambda state: state._sc2wol_has_common_unit(world, player) and lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_anti_air(world, player) and state._sc2wol_has_anti_air(world, player) and
state._sc2wol_has_heavy_defense(world, player)), state._sc2wol_has_heavy_defense(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1000, LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
lambda state: state._sc2wol_has_air(world, player)), lambda state: state._sc2wol_has_air(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001, LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001,
lambda state: state._sc2wol_has_air(world, player) or True), lambda state: state._sc2wol_has_air(world, player) or True),
@ -131,7 +131,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Supernova", "Beat Supernova", None, LocationData("Supernova", "Beat Supernova", None,
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Maw of the Void", "Maw of the Void: Xel'Naga Vault", SC2WOL_LOC_ID_OFFSET + 1200, LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200,
lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and
state._sc2wol_has_air(world, player)), state._sc2wol_has_air(world, 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: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201),
@ -148,14 +148,14 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData("Maw of the Void", "Beat Maw of the Void", None, LocationData("Maw of the Void", "Beat Maw of the Void", None,
lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and
state._sc2wol_has_air(world, player)), state._sc2wol_has_air(world, player)),
LocationData("Devil's Playground", "Devil's Playground: 8000 Minerals", SC2WOL_LOC_ID_OFFSET + 1300, LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), lambda state: state._sc2wol_has_common_unit(world, 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: 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: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
LocationData("Devil's Playground", "Beat Devil's Playground", None, LocationData("Devil's Playground", "Beat Devil's Playground", None,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: 7 Canisters", SC2WOL_LOC_ID_OFFSET + 1400, LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400,
lambda state: state._sc2wol_has_common_unit(world, player) and lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_mobile_anti_air(world, player)), state._sc2wol_has_mobile_anti_air(world, 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: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401),
@ -168,25 +168,25 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData("Welcome to the Jungle", "Beat Welcome to the Jungle", None, LocationData("Welcome to the Jungle", "Beat Welcome to the Jungle", None,
lambda state: state._sc2wol_has_common_unit(world, player) and lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_mobile_anti_air(world, player)), state._sc2wol_has_mobile_anti_air(world, player)),
LocationData("Breakout", "Breakout: Main Prison", SC2WOL_LOC_ID_OFFSET + 1500), LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500),
LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501), LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501),
LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502), LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502),
LocationData("Breakout", "Beat Breakout", None), LocationData("Breakout", "Beat Breakout", None),
LocationData("Ghost of a Chance", "Ghost of a Chance: Psi-Indoctrinator", SC2WOL_LOC_ID_OFFSET + 1600), LocationData("Ghost of a Chance", "Ghost of a Chance: 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: 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: 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: 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: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604),
LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605), LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605),
LocationData("Ghost of a Chance", "Beat Ghost of a Chance", None), LocationData("Ghost of a Chance", "Beat Ghost of a Chance", None),
LocationData("The Great Train Robbery", "The Great Train Robbery: 8 Trains", SC2WOL_LOC_ID_OFFSET + 1700, LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700,
lambda state: state._sc2wol_has_train_killers(world, player)), lambda state: state._sc2wol_has_train_killers(world, player)),
LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701), LocationData("The Great Train Robbery", "The Great Train Robbery: 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: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702),
LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703), LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703),
LocationData("The Great Train Robbery", "Beat The Great Train Robbery", None, LocationData("The Great Train Robbery", "Beat The Great Train Robbery", None,
lambda state: state._sc2wol_has_train_killers(world, player)), lambda state: state._sc2wol_has_train_killers(world, player)),
LocationData("Cutthroat", "Cutthroat: Orlan's Planetary", SC2WOL_LOC_ID_OFFSET + 1800, LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800,
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801,
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
@ -197,7 +197,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Cutthroat", "Beat Cutthroat", None, LocationData("Cutthroat", "Beat Cutthroat", None,
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Engine of Destruction", "Engine of Destruction: Dominion Bases", SC2WOL_LOC_ID_OFFSET + 1900, LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900,
lambda state: state._sc2wol_has_mobile_anti_air(world, player)), lambda state: state._sc2wol_has_mobile_anti_air(world, player)),
LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901), LocationData("Engine of Destruction", "Engine of Destruction: 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: Loki", SC2WOL_LOC_ID_OFFSET + 1902,
@ -213,7 +213,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData("Engine of Destruction", "Beat Engine of Destruction", None, LocationData("Engine of Destruction", "Beat Engine of Destruction", None,
lambda state: state._sc2wol_has_mobile_anti_air(world, player) and lambda state: state._sc2wol_has_mobile_anti_air(world, player) and
state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)),
LocationData("Media Blitz", "Media Blitz: Full Upload", SC2WOL_LOC_ID_OFFSET + 2000, LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000,
lambda state: state._sc2wol_has_competent_comp(world, player)), lambda state: state._sc2wol_has_competent_comp(world, 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,
lambda state: state._sc2wol_has_competent_comp(world, player)), lambda state: state._sc2wol_has_competent_comp(world, player)),
@ -224,19 +224,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004), LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004),
LocationData("Media Blitz", "Beat Media Blitz", None, LocationData("Media Blitz", "Beat Media Blitz", None,
lambda state: state._sc2wol_has_competent_comp(world, player)), lambda state: state._sc2wol_has_competent_comp(world, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: Facility Escape", SC2WOL_LOC_ID_OFFSET + 2100), LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100),
LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101), LocationData("Piercing the Shroud", "Piercing the Shroud: 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: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102),
LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103), LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103),
LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104), LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104),
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105), LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105),
LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None), LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None),
LocationData("Whispers of Doom", "Whispers of Doom: Void Seeker Escape", SC2WOL_LOC_ID_OFFSET + 2200), LocationData("Whispers of Doom", "Whispers of Doom: 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: 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: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202),
LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203), LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203),
LocationData("Whispers of Doom", "Beat Whispers of Doom", None), LocationData("Whispers of Doom", "Beat Whispers of Doom", None),
LocationData("A Sinister Turn", "A Sinister Turn: Preservers Freed", SC2WOL_LOC_ID_OFFSET + 2300, LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)), lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301), LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301),
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302), LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302),
@ -244,31 +244,31 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
lambda state: state._sc2wol_has_protoss_common_units(world, player)), lambda state: state._sc2wol_has_protoss_common_units(world, player)),
LocationData("A Sinister Turn", "Beat A Sinister Turn", None, LocationData("A Sinister Turn", "Beat A Sinister Turn", None,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)), lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("Echoes of the Future", "Echoes of the Future: Overmind", SC2WOL_LOC_ID_OFFSET + 2400, LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)), lambda state: state._sc2wol_has_protoss_medium_units(world, 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: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401),
LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402,
lambda state: state._sc2wol_has_protoss_common_units(world, player)), lambda state: state._sc2wol_has_protoss_common_units(world, player)),
LocationData("Echoes of the Future", "Beat Echoes of the Future", None, LocationData("Echoes of the Future", "Beat Echoes of the Future", None,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)), lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2500, LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)), lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)), lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2502), LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502),
LocationData("In Utter Darkness", "Beat In Utter Darkness", None, LocationData("In Utter Darkness", "Beat In Utter Darkness", None,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)), lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("Gates of Hell", "Gates of Hell: Nydus Worms", SC2WOL_LOC_ID_OFFSET + 2600, LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600,
lambda state: state._sc2wol_has_competent_comp(world, player)), lambda state: state._sc2wol_has_competent_comp(world, player)),
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,
lambda state: state._sc2wol_has_competent_comp(world, player)), lambda state: state._sc2wol_has_competent_comp(world, player)),
LocationData("Gates of Hell", "Beat Gates of Hell", None), LocationData("Gates of Hell", "Beat Gates of Hell", None),
LocationData("Belly of the Beast", "Belly of the Beast: Extract", SC2WOL_LOC_ID_OFFSET + 2700), LocationData("Belly of the Beast", "Belly of the Beast: 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: 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: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702),
LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703), LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703),
LocationData("Belly of the Beast", "Beat Belly of the Beast", None), LocationData("Belly of the Beast", "Beat Belly of the Beast", None),
LocationData("Shatter the Sky", "Shatter the Sky: Platform Destroyed", SC2WOL_LOC_ID_OFFSET + 2800, LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800,
lambda state: state._sc2wol_has_competent_comp(world, player)), lambda state: state._sc2wol_has_competent_comp(world, 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,
lambda state: state._sc2wol_has_competent_comp(world, player)), lambda state: state._sc2wol_has_competent_comp(world, player)),

View File

@ -0,0 +1,96 @@
from typing import NamedTuple, Dict, List
no_build_regions_list = ["Liberation Day", "Breakout", "Ghost of a Chance", "Piercing the Shroud", "Whispers of Doom",
"Belly of the Beast"]
easy_regions_list = ["The Outlaws", "Zero Hour", "Evacuation", "Outbreak", "Smash and Grab", "Devil's Playground"]
medium_regions_list = ["Safe Haven", "Haven's Fall", "The Dig", "The Moebius Factor", "Supernova",
"Welcome to the Jungle", "The Great Train Robbery", "Cutthroat", "Media Blitz",
"A Sinister Turn", "Echoes of the Future"]
hard_regions_list = ["Maw of the Void", "Engine of Destruction", "In Utter Darkness", "Gates of Hell",
"Shatter the Sky"]
class MissionInfo(NamedTuple):
id: int
extra_locations: int
required_world: List[int]
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
class FillMission(NamedTuple):
type: str
connect_to: List[int] # -1 connects to Menu
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
vanilla_shuffle_order = [
FillMission("no_build", [-1], completion_critical=True),
FillMission("easy", [0], completion_critical=True),
FillMission("easy", [1], completion_critical=True),
FillMission("easy", [2]),
FillMission("medium", [3]),
FillMission("hard", [4], number=7),
FillMission("hard", [4], number=7),
FillMission("easy", [2], completion_critical=True),
FillMission("medium", [7], number=8, completion_critical=True),
FillMission("hard", [8], number=11, completion_critical=True),
FillMission("hard", [9], number=14, completion_critical=True),
FillMission("hard", [10], completion_critical=True),
FillMission("medium", [2], number=4),
FillMission("medium", [12]),
FillMission("hard", [13], number=8),
FillMission("hard", [13], number=8),
FillMission("medium", [2], number=6),
FillMission("hard", [16]),
FillMission("hard", [17]),
FillMission("hard", [18]),
FillMission("hard", [19]),
FillMission("medium", [8]),
FillMission("hard", [21]),
FillMission("hard", [22]),
FillMission("hard", [23]),
FillMission("hard", [11], completion_critical=True),
FillMission("hard", [25], completion_critical=True),
FillMission("hard", [25], completion_critical=True),
FillMission("all_in", [26, 27], completion_critical=True, or_requirements=True)
]
vanilla_mission_req_table = {
"Liberation Day": MissionInfo(1, 7, [], completion_critical=True),
"The Outlaws": MissionInfo(2, 2, [1], completion_critical=True),
"Zero Hour": MissionInfo(3, 4, [2], completion_critical=True),
"Evacuation": MissionInfo(4, 4, [3]),
"Outbreak": MissionInfo(5, 3, [4]),
"Safe Haven": MissionInfo(6, 1, [5], number=7),
"Haven's Fall": MissionInfo(7, 1, [5], number=7),
"Smash and Grab": MissionInfo(8, 5, [3], completion_critical=True),
"The Dig": MissionInfo(9, 4, [8], number=8, completion_critical=True),
"The Moebius Factor": MissionInfo(10, 9, [9], number=11, completion_critical=True),
"Supernova": MissionInfo(11, 5, [10], number=14, completion_critical=True),
"Maw of the Void": MissionInfo(12, 6, [11], completion_critical=True),
"Devil's Playground": MissionInfo(13, 3, [3], number=4),
"Welcome to the Jungle": MissionInfo(14, 4, [13]),
"Breakout": MissionInfo(15, 3, [14], number=8),
"Ghost of a Chance": MissionInfo(16, 6, [14], number=8),
"The Great Train Robbery": MissionInfo(17, 4, [3], number=6),
"Cutthroat": MissionInfo(18, 5, [17]),
"Engine of Destruction": MissionInfo(19, 6, [18]),
"Media Blitz": MissionInfo(20, 5, [19]),
"Piercing the Shroud": MissionInfo(21, 6, [20]),
"Whispers of Doom": MissionInfo(22, 4, [9]),
"A Sinister Turn": MissionInfo(23, 4, [22]),
"Echoes of the Future": MissionInfo(24, 3, [23]),
"In Utter Darkness": MissionInfo(25, 3, [24]),
"Gates of Hell": MissionInfo(26, 2, [12], completion_critical=True),
"Belly of the Beast": MissionInfo(27, 4, [26], completion_critical=True),
"Shatter the Sky": MissionInfo(28, 5, [26], completion_critical=True),
"All-In": MissionInfo(29, -1, [27, 28], completion_critical=True, or_requirements=True)
}
lookup_id_to_mission: Dict[int, str] = {
data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id}

View File

@ -1,6 +1,6 @@
from typing import Dict from typing import Dict
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from Options import Choice, Option from Options import Choice, Option, DefaultOnToggle
class GameDifficulty(Choice): class GameDifficulty(Choice):
@ -35,12 +35,28 @@ class AllInMap(Choice):
option_air = 1 option_air = 1
class MissionOrder(Choice):
"""Determines the order the missions are played in.
Vanilla: Keeps the standard mission order and branching from the WoL Campaign.
Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within"""
display_name = "Mission Order"
option_vanilla = 0
option_vanilla_shuffled = 1
class ShuffleProtoss(DefaultOnToggle):
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla Shuffled is enabled. If this is
not the 3 protoss missions will stay in their vanilla order in the mission order making them optional to complete
the game."""
display_name = "Shuffle Protoss Missions"
# noinspection PyTypeChecker # noinspection PyTypeChecker
sc2wol_options: Dict[str, Option] = { sc2wol_options: Dict[str, Option] = {
"game_difficulty": GameDifficulty, "game_difficulty": GameDifficulty,
"upgrade_bonus": UpgradeBonus, "upgrade_bonus": UpgradeBonus,
"bunker_upgrade": BunkerUpgrade, "bunker_upgrade": BunkerUpgrade,
"all_in_map": AllInMap, "all_in_map": AllInMap,
"mission_order": MissionOrder,
"shuffle_protoss": ShuffleProtoss
} }

View File

@ -1,6 +1,10 @@
from typing import List, Set, Dict, Tuple, Optional, Callable from typing import List, Set, Dict, Tuple, Optional, Callable, NamedTuple
from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
from .Locations import LocationData from .Locations import LocationData
from .Options import get_option_value
from worlds.sc2wol.MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \
no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list
import random
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location]): def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location]):
@ -46,73 +50,163 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
names: Dict[str, int] = {} names: Dict[str, int] = {}
connect(world, player, names, 'Menu', 'Liberation Day'), if get_option_value(world, player, "mission_order") == 0:
connect(world, player, names, 'Liberation Day', 'The Outlaws', connect(world, player, names, 'Menu', 'Liberation Day'),
lambda state: state.has("Beat Liberation Day", player)), connect(world, player, names, 'Liberation Day', 'The Outlaws',
connect(world, player, names, 'The Outlaws', 'Zero Hour', lambda state: state.has("Beat Liberation Day", player)),
lambda state: state.has("Beat The Outlaws", player)), connect(world, player, names, 'The Outlaws', 'Zero Hour',
connect(world, player, names, 'Zero Hour', 'Evacuation', lambda state: state.has("Beat The Outlaws", player)),
lambda state: state.has("Beat Zero Hour", player)), connect(world, player, names, 'Zero Hour', 'Evacuation',
connect(world, player, names, 'Evacuation', 'Outbreak', lambda state: state.has("Beat Zero Hour", player)),
lambda state: state.has("Beat Evacuation", player)), connect(world, player, names, 'Evacuation', 'Outbreak',
connect(world, player, names, "Outbreak", "Safe Haven", lambda state: state.has("Beat Evacuation", player)),
lambda state: state._sc2wol_cleared_missions(world, player, 7) and connect(world, player, names, "Outbreak", "Safe Haven",
state.has("Beat Outbreak", player)), lambda state: state._sc2wol_cleared_missions(world, player, 7) and
connect(world, player, names, "Outbreak", "Haven's Fall", state.has("Beat Outbreak", player)),
lambda state: state._sc2wol_cleared_missions(world, player, 7) and connect(world, player, names, "Outbreak", "Haven's Fall",
state.has("Beat Outbreak", player)), lambda state: state._sc2wol_cleared_missions(world, player, 7) and
connect(world, player, names, 'Zero Hour', 'Smash and Grab', state.has("Beat Outbreak", player)),
lambda state: state.has("Beat Zero Hour", player)), connect(world, player, names, 'Zero Hour', 'Smash and Grab',
connect(world, player, names, 'Smash and Grab', 'The Dig', lambda state: state.has("Beat Zero Hour", player)),
lambda state: state._sc2wol_cleared_missions(world, player, 8) and connect(world, player, names, 'Smash and Grab', 'The Dig',
state.has("Beat Smash and Grab", player)), lambda state: state._sc2wol_cleared_missions(world, player, 8) and
connect(world, player, names, 'The Dig', 'The Moebius Factor', state.has("Beat Smash and Grab", player)),
lambda state: state._sc2wol_cleared_missions(world, player, 11) and connect(world, player, names, 'The Dig', 'The Moebius Factor',
state.has("Beat The Dig", player)), lambda state: state._sc2wol_cleared_missions(world, player, 11) and
connect(world, player, names, 'The Moebius Factor', 'Supernova', state.has("Beat The Dig", player)),
lambda state: state._sc2wol_cleared_missions(world, player, 14) and connect(world, player, names, 'The Moebius Factor', 'Supernova',
state.has("Beat The Moebius Factor", player)), lambda state: state._sc2wol_cleared_missions(world, player, 14) and
connect(world, player, names, 'Supernova', 'Maw of the Void', state.has("Beat The Moebius Factor", player)),
lambda state: state.has("Beat Supernova", player)), connect(world, player, names, 'Supernova', 'Maw of the Void',
connect(world, player, names, 'Zero Hour', "Devil's Playground", lambda state: state.has("Beat Supernova", player)),
lambda state: state._sc2wol_cleared_missions(world, player, 4) and connect(world, player, names, 'Zero Hour', "Devil's Playground",
state.has("Beat Zero Hour", player)), lambda state: state._sc2wol_cleared_missions(world, player, 4) and
connect(world, player, names, "Devil's Playground", 'Welcome to the Jungle', state.has("Beat Zero Hour", player)),
lambda state: state.has("Beat Devil's Playground", player)), connect(world, player, names, "Devil's Playground", 'Welcome to the Jungle',
connect(world, player, names, "Welcome to the Jungle", 'Breakout', lambda state: state.has("Beat Devil's Playground", player)),
lambda state: state._sc2wol_cleared_missions(world, player, 8) and connect(world, player, names, "Welcome to the Jungle", 'Breakout',
state.has("Beat Welcome to the Jungle", player)), lambda state: state._sc2wol_cleared_missions(world, player, 8) and
connect(world, player, names, "Welcome to the Jungle", 'Ghost of a Chance', state.has("Beat Welcome to the Jungle", player)),
lambda state: state._sc2wol_cleared_missions(world, player, 8) and connect(world, player, names, "Welcome to the Jungle", 'Ghost of a Chance',
state.has("Beat Welcome to the Jungle", player)), lambda state: state._sc2wol_cleared_missions(world, player, 8) and
connect(world, player, names, "Zero Hour", 'The Great Train Robbery', state.has("Beat Welcome to the Jungle", player)),
lambda state: state._sc2wol_cleared_missions(world, player, 6) and connect(world, player, names, "Zero Hour", 'The Great Train Robbery',
state.has("Beat Zero Hour", player)), lambda state: state._sc2wol_cleared_missions(world, player, 6) and
connect(world, player, names, 'The Great Train Robbery', 'Cutthroat', state.has("Beat Zero Hour", player)),
lambda state: state.has("Beat The Great Train Robbery", player)), connect(world, player, names, 'The Great Train Robbery', 'Cutthroat',
connect(world, player, names, 'Cutthroat', 'Engine of Destruction', lambda state: state.has("Beat The Great Train Robbery", player)),
lambda state: state.has("Beat Cutthroat", player)), connect(world, player, names, 'Cutthroat', 'Engine of Destruction',
connect(world, player, names, 'Engine of Destruction', 'Media Blitz', lambda state: state.has("Beat Cutthroat", player)),
lambda state: state.has("Beat Engine of Destruction", player)), connect(world, player, names, 'Engine of Destruction', 'Media Blitz',
connect(world, player, names, 'Media Blitz', 'Piercing the Shroud', lambda state: state.has("Beat Engine of Destruction", player)),
lambda state: state.has("Beat Media Blitz", player)), connect(world, player, names, 'Media Blitz', 'Piercing the Shroud',
connect(world, player, names, 'The Dig', 'Whispers of Doom', lambda state: state.has("Beat Media Blitz", player)),
lambda state: state.has("Beat The Dig", player)), connect(world, player, names, 'The Dig', 'Whispers of Doom',
connect(world, player, names, 'Whispers of Doom', 'A Sinister Turn', lambda state: state.has("Beat The Dig", player)),
lambda state: state.has("Beat Whispers of Doom", player)), connect(world, player, names, 'Whispers of Doom', 'A Sinister Turn',
connect(world, player, names, 'A Sinister Turn', 'Echoes of the Future', lambda state: state.has("Beat Whispers of Doom", player)),
lambda state: state.has("Beat A Sinister Turn", player)), connect(world, player, names, 'A Sinister Turn', 'Echoes of the Future',
connect(world, player, names, 'Echoes of the Future', 'In Utter Darkness', lambda state: state.has("Beat A Sinister Turn", player)),
lambda state: state.has("Beat Echoes of the Future", player)), connect(world, player, names, 'Echoes of the Future', 'In Utter Darkness',
connect(world, player, names, 'Maw of the Void', 'Gates of Hell', lambda state: state.has("Beat Echoes of the Future", player)),
lambda state: state.has("Beat Maw of the Void", player)), connect(world, player, names, 'Maw of the Void', 'Gates of Hell',
connect(world, player, names, 'Gates of Hell', 'Belly of the Beast', lambda state: state.has("Beat Maw of the Void", player)),
lambda state: state.has("Beat Gates of Hell", player)), connect(world, player, names, 'Gates of Hell', 'Belly of the Beast',
connect(world, player, names, 'Gates of Hell', 'Shatter the Sky', lambda state: state.has("Beat Gates of Hell", player)),
lambda state: state.has("Beat Gates of Hell", player)), connect(world, player, names, 'Gates of Hell', 'Shatter the Sky',
connect(world, player, names, 'Gates of Hell', 'All-In', lambda state: state.has("Beat Gates of Hell", player)),
lambda state: state.has('Beat Gates of Hell', player) and ( connect(world, player, names, 'Gates of Hell', 'All-In',
state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player))) lambda state: state.has('Beat Gates of Hell', player) and (
state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player)))
return vanilla_mission_req_table
elif get_option_value(world, player, "mission_order") == 1:
missions = []
no_build_pool = no_build_regions_list[:]
easy_pool = easy_regions_list[:]
medium_pool = medium_regions_list[:]
hard_pool = hard_regions_list[:]
# Initial fill out of mission list and marking all-in mission
for mission in vanilla_shuffle_order:
if mission.type == "all_in":
missions.append("All-In")
else:
missions.append(mission.type)
# Place Protoss Missions if we are not using ShuffleProtoss
if get_option_value(world, player, "shuffle_protoss") == 0:
missions[22] = "A Sinister Turn"
medium_pool.remove("A Sinister Turn")
missions[23] = "Echoes of the Future"
medium_pool.remove("Echoes of the Future")
missions[24] = "In Utter Darkness"
hard_pool.remove("In Utter Darkness")
no_build_slots = []
easy_slots = []
medium_slots = []
hard_slots = []
# Search through missions to find slots needed to fill
for i in range(len(missions)):
if missions[i] == "no_build":
no_build_slots.append(i)
elif missions[i] == "easy":
easy_slots.append(i)
elif missions[i] == "medium":
medium_slots.append(i)
elif missions[i] == "hard":
hard_slots.append(i)
# Add no_build missions to the pool and fill in no_build slots
missions_to_add = no_build_pool
for slot in no_build_slots:
filler = random.randint(0, len(missions_to_add)-1)
missions[slot] = missions_to_add.pop(filler)
# Add easy missions into pool and fill in easy slots
missions_to_add = missions_to_add + easy_pool
for slot in easy_slots:
filler = random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Add medium missions into pool and fill in medium slots
missions_to_add = missions_to_add + medium_pool
for slot in medium_slots:
filler = random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Add hard missions into pool and fill in hard slots
missions_to_add = missions_to_add + hard_pool
for slot in hard_slots:
filler = random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Loop through missions to create requirements table and connect regions
# TODO: Handle 'and' connections
mission_req_table = {}
for i in range(len(missions)):
connections = []
for connection in vanilla_shuffle_order[i].connect_to:
if connection == -1:
connect(world, player, names, "Menu", missions[i])
else:
connect(world, player, names, missions[connection], missions[i],
(lambda name: (lambda state: state.has(f"Beat {name}", player)))(missions[connection]))
connections.append(connection + 1)
mission_req_table.update({missions[i]: MissionInfo(
vanilla_mission_req_table[missions[i]].id, vanilla_mission_req_table[missions[i]].extra_locations,
connections, completion_critical=vanilla_shuffle_order[i].completion_critical,
or_requirements=vanilla_shuffle_order[i].or_requirements)})
return mission_req_table
def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]):

View File

@ -1,6 +1,6 @@
import typing import typing
from typing import List, Set, Tuple from typing import List, Set, Tuple, NamedTuple
from BaseClasses import Item, MultiWorld, Location, Tutorial from BaseClasses import Item, MultiWorld, Location, Tutorial
from ..AutoWorld import World, WebWorld from ..AutoWorld import World, WebWorld
from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \ from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \
@ -24,6 +24,7 @@ class Starcraft2WoLWebWorld(WebWorld):
tutorials = [setup] tutorials = [setup]
class SC2WoLWorld(World): class SC2WoLWorld(World):
""" """
StarCraft II: Wings of Liberty is a science fiction real-time strategy video game developed and published by Blizzard Entertainment. StarCraft II: Wings of Liberty is a science fiction real-time strategy video game developed and published by Blizzard Entertainment.
@ -40,6 +41,7 @@ class SC2WoLWorld(World):
item_name_groups = item_name_groups item_name_groups = item_name_groups
locked_locations: typing.List[str] locked_locations: typing.List[str]
location_cache: typing.List[Location] location_cache: typing.List[Location]
mission_req_table = {}
def __init__(self, world: MultiWorld, player: int): def __init__(self, world: MultiWorld, player: int):
super(SC2WoLWorld, self).__init__(world, player) super(SC2WoLWorld, self).__init__(world, player)
@ -55,8 +57,8 @@ class SC2WoLWorld(World):
return StarcraftWoLItem(name, data.progression, data.code, self.player) return StarcraftWoLItem(name, data.progression, data.code, self.player)
def create_regions(self): def create_regions(self):
create_regions(self.world, self.player, get_locations(self.world, self.player), self.mission_req_table = create_regions(self.world, self.player, get_locations(self.world, self.player),
self.location_cache) self.location_cache)
def generate_basic(self): def generate_basic(self):
excluded_items = get_excluded_items(self, self.world, self.player) excluded_items = get_excluded_items(self, self.world, self.player)
@ -83,6 +85,11 @@ class SC2WoLWorld(World):
option = getattr(self.world, option_name)[self.player] option = getattr(self.world, option_name)[self.player]
if type(option.value) in {str, int}: if type(option.value) in {str, int}:
slot_data[option_name] = int(option.value) slot_data[option_name] = int(option.value)
slot_req_table = {}
for mission in self.mission_req_table:
slot_req_table[mission] = self.mission_req_table[mission]._asdict()
slot_data["mission_req"] = slot_req_table
return slot_data return slot_data
@ -122,7 +129,15 @@ def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str
if not local_basic_unit: if not local_basic_unit:
raise Exception("At least one basic unit must be local") raise Exception("At least one basic unit must be local")
assign_starter_item(world, player, excluded_items, locked_locations, 'Liberation Day: First Statue', # The first world should also be the starting world
first_location = list(world.worlds[player].mission_req_table)[0]
if first_location == "In Utter Darkness":
first_location = first_location + ": Defeat"
else:
first_location = first_location + ": Victory"
assign_starter_item(world, player, excluded_items, locked_locations, first_location,
local_basic_unit) local_basic_unit)
@ -168,4 +183,4 @@ def create_item_with_correct_settings(world: MultiWorld, player: int, name: str)
if not item.advancement: if not item.advancement:
return item return item
return item return item