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.bot_ai import BotAI
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.Locations import SC2WOL_LOC_ID_OFFSET
@ -32,6 +34,13 @@ nest_asyncio.apply()
class StarcraftClientProcessor(ClientCommandProcessor):
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:
"""Start a Starcraft 2 mission"""
@ -42,7 +51,8 @@ class StarcraftClientProcessor(ClientCommandProcessor):
if num_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 not self.ctx.sc2_run_task.done():
sc2_logger.warning("Starcraft 2 Client is still running!")
@ -65,13 +75,13 @@ class StarcraftClientProcessor(ClientCommandProcessor):
def _cmd_available(self) -> bool:
"""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
def _cmd_unfinished(self) -> bool:
"""Get what missions are currently available to play and have not had all locations checked"""
request_unfinished_missions(self.ctx.checked_locations, mission_req_table, self.ctx.ui)
request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx)
return True
@ -81,6 +91,7 @@ class Context(CommonContext):
items_handling = 0b111
difficulty = -1
all_in_choice = 0
mission_req_table = None
items_rec_to_announce = []
rec_announce_pos = 0
items_sent_to_announce = []
@ -102,6 +113,11 @@ class Context(CommonContext):
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"]
self.mission_req_table = {}
for mission in slot_req_table:
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
if cmd in {"PrintJSON"}:
noted = False
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.")
run_game(sc2.maps.get(maps_table[mission_id - 1]), [
Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), name="Archipelago", fullscreen=True)], realtime=True)
run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
name="Archipelago", fullscreen=True)], realtime=True)
class ArchipelagoBot(sc2.bot_ai.BotAI):
@ -302,7 +318,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
game_state = int(38281 - unit.health)
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 " +
"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.")
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 = {
"Liberation Day": MissionInfo(1, 7, [], 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)
}
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):
def calc_objectives_completed(mission, missions_info, locations_done, unfinished_locations, ctx):
objectives_complete = 0
if missions_info[mission].extra_locations > 0:
for i in range(missions_info[mission].extra_locations):
if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done:
objectives_complete += 1
else:
unfinished_locations[mission].append(ctx.location_name_getter(
missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i))
return objectives_complete
@ -449,31 +456,37 @@ def calc_objectives_completed(mission, missions_info, locations_done):
return -1
def request_unfinished_missions(locations_done, location_table, ui):
message = "Unfinished Missions: "
def request_unfinished_missions(locations_done, location_table, ui, ctx):
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}] "
f"({unfinished_missions[mission]}/{location_table[mission].extra_locations})"
for mission in unfinished_missions)
if ui:
ui.log_panels['All'].on_message_markup(message)
ui.log_panels['Starcraft2'].on_message_markup(message)
if ui:
ui.log_panels['All'].on_message_markup(message)
ui.log_panels['Starcraft2'].on_message_markup(message)
else:
sc2_logger.info(message)
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 = []
locations_completed = []
available_missions = calc_available_missions(locations_done, locations)
available_missions = calc_available_missions(locations_done, locations, unlocks)
for name in available_missions:
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:
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)
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."""
if location_table[mission].completion_critical:
if ui:
return "[color=AF99EF]" + mission + "[/color]"
message = "[color=AF99EF]" + mission + "[/color]"
else:
return "*" + mission + "*"
message = "*" + mission + "*"
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):
message = "Available Missions: "
if location_table:
message = "Available Missions: "
missions = calc_available_missions(locations_done, location_table)
message += ", ".join(f"{mark_critical(mission,location_table, ui)}[{location_table[mission].id}]" for mission in missions)
# Initialize mission unlock table
unlocks = initialize_blank_mission_dict(location_table)
if ui:
ui.log_panels['All'].on_message_markup(message)
ui.log_panels['Starcraft2'].on_message_markup(message)
missions = calc_available_missions(locations_done, location_table, unlocks)
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:
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 = []
missions_complete = 0
@ -526,6 +573,11 @@ def calc_available_missions(locations_done, locations):
missions_complete += 1
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):
available_missions.append(name)
@ -549,14 +601,14 @@ def mission_reqs_completed(location_to_check, missions_complete, locations_done,
req_success = True
# 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:
return False
else:
req_success = False
# 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):
if not locations[location_to_check].or_requirements:
return False
@ -581,6 +633,15 @@ def mission_reqs_completed(location_to_check, missions_complete, locations_done,
return True
def initialize_blank_mission_dict(location_table):
unlocks = {}
for mission in list(location_table):
unlocks[mission] = []
return unlocks
if __name__ == '__main__':
colorama.init()
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
state._sc2wol_has_anti_air(world, player) and
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)),
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),
@ -131,7 +131,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Supernova", "Beat Supernova", None,
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
state._sc2wol_has_air(world, player)),
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,
lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and
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)),
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,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
LocationData("Devil's Playground", "Beat Devil's Playground", None,
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
state._sc2wol_has_mobile_anti_air(world, player)),
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,
lambda state: state._sc2wol_has_common_unit(world, player) and
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: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502),
LocationData("Breakout", "Beat Breakout", None),
LocationData("Ghost of a Chance", "Ghost of a Chance: Psi-Indoctrinator", SC2WOL_LOC_ID_OFFSET + 1600),
LocationData("Ghost of a Chance", "Ghost of a Chance: 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("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)),
LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701),
LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702),
LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703),
LocationData("The Great Train Robbery", "Beat The Great Train Robbery", None,
lambda state: state._sc2wol_has_train_killers(world, player)),
LocationData("Cutthroat", "Cutthroat: Orlan's Planetary", SC2WOL_LOC_ID_OFFSET + 1800,
LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801,
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)),
LocationData("Cutthroat", "Beat Cutthroat", None,
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)),
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,
@ -213,7 +213,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData("Engine of Destruction", "Beat Engine of Destruction", None,
lambda state: state._sc2wol_has_mobile_anti_air(world, player) and
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)),
LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001,
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", "Beat Media Blitz", None,
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: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102),
LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103),
LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104),
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105),
LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None),
LocationData("Whispers of Doom", "Whispers of Doom: Void Seeker Escape", SC2WOL_LOC_ID_OFFSET + 2200),
LocationData("Whispers of Doom", "Whispers of Doom: 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("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)),
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),
@ -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)),
LocationData("A Sinister Turn", "Beat A Sinister Turn", None,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("Echoes of the Future", "Echoes of the Future: Overmind", SC2WOL_LOC_ID_OFFSET + 2400,
LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400,
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: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402,
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
LocationData("Echoes of the Future", "Beat Echoes of the Future", None,
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)),
LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501,
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,
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)),
LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601,
lambda state: state._sc2wol_has_competent_comp(world, player)),
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: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702),
LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703),
LocationData("Belly of the Beast", "Beat Belly of the Beast", None),
LocationData("Shatter the Sky", "Shatter the Sky: Platform Destroyed", SC2WOL_LOC_ID_OFFSET + 2800,
LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800,
lambda state: state._sc2wol_has_competent_comp(world, player)),
LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801,
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 BaseClasses import MultiWorld
from Options import Choice, Option
from Options import Choice, Option, DefaultOnToggle
class GameDifficulty(Choice):
@ -35,12 +35,28 @@ class AllInMap(Choice):
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
sc2wol_options: Dict[str, Option] = {
"game_difficulty": GameDifficulty,
"upgrade_bonus": UpgradeBonus,
"bunker_upgrade": BunkerUpgrade,
"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 .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]):
@ -46,73 +50,163 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
names: Dict[str, int] = {}
connect(world, player, names, 'Menu', 'Liberation Day'),
connect(world, player, names, 'Liberation Day', 'The Outlaws',
lambda state: state.has("Beat Liberation Day", player)),
connect(world, player, names, 'The Outlaws', 'Zero Hour',
lambda state: state.has("Beat The Outlaws", player)),
connect(world, player, names, 'Zero Hour', 'Evacuation',
lambda state: state.has("Beat Zero Hour", player)),
connect(world, player, names, 'Evacuation', 'Outbreak',
lambda state: state.has("Beat Evacuation", player)),
connect(world, player, names, "Outbreak", "Safe Haven",
lambda state: state._sc2wol_cleared_missions(world, player, 7) and
state.has("Beat Outbreak", player)),
connect(world, player, names, "Outbreak", "Haven's Fall",
lambda state: state._sc2wol_cleared_missions(world, player, 7) and
state.has("Beat Outbreak", player)),
connect(world, player, names, 'Zero Hour', 'Smash and Grab',
lambda state: state.has("Beat Zero Hour", player)),
connect(world, player, names, 'Smash and Grab', 'The Dig',
lambda state: state._sc2wol_cleared_missions(world, player, 8) and
state.has("Beat Smash and Grab", player)),
connect(world, player, names, 'The Dig', 'The Moebius Factor',
lambda state: state._sc2wol_cleared_missions(world, player, 11) and
state.has("Beat The Dig", player)),
connect(world, player, names, 'The Moebius Factor', 'Supernova',
lambda state: state._sc2wol_cleared_missions(world, player, 14) and
state.has("Beat The Moebius Factor", player)),
connect(world, player, names, 'Supernova', 'Maw of the Void',
lambda state: state.has("Beat Supernova", player)),
connect(world, player, names, 'Zero Hour', "Devil's Playground",
lambda state: state._sc2wol_cleared_missions(world, player, 4) and
state.has("Beat Zero Hour", player)),
connect(world, player, names, "Devil's Playground", 'Welcome to the Jungle',
lambda state: state.has("Beat Devil's Playground", player)),
connect(world, player, names, "Welcome to the Jungle", 'Breakout',
lambda state: state._sc2wol_cleared_missions(world, player, 8) and
state.has("Beat Welcome to the Jungle", player)),
connect(world, player, names, "Welcome to the Jungle", 'Ghost of a Chance',
lambda state: state._sc2wol_cleared_missions(world, player, 8) and
state.has("Beat Welcome to the Jungle", player)),
connect(world, player, names, "Zero Hour", 'The Great Train Robbery',
lambda state: state._sc2wol_cleared_missions(world, player, 6) and
state.has("Beat Zero Hour", player)),
connect(world, player, names, 'The Great Train Robbery', 'Cutthroat',
lambda state: state.has("Beat The Great Train Robbery", player)),
connect(world, player, names, 'Cutthroat', 'Engine of Destruction',
lambda state: state.has("Beat Cutthroat", player)),
connect(world, player, names, 'Engine of Destruction', 'Media Blitz',
lambda state: state.has("Beat Engine of Destruction", player)),
connect(world, player, names, 'Media Blitz', 'Piercing the Shroud',
lambda state: state.has("Beat Media Blitz", player)),
connect(world, player, names, 'The Dig', 'Whispers of Doom',
lambda state: state.has("Beat The Dig", player)),
connect(world, player, names, 'Whispers of Doom', 'A Sinister Turn',
lambda state: state.has("Beat Whispers of Doom", player)),
connect(world, player, names, 'A Sinister Turn', 'Echoes of the Future',
lambda state: state.has("Beat A Sinister Turn", player)),
connect(world, player, names, 'Echoes of the Future', 'In Utter Darkness',
lambda state: state.has("Beat Echoes of the Future", player)),
connect(world, player, names, 'Maw of the Void', 'Gates of Hell',
lambda state: state.has("Beat Maw of the Void", player)),
connect(world, player, names, 'Gates of Hell', 'Belly of the Beast',
lambda state: state.has("Beat Gates of Hell", player)),
connect(world, player, names, 'Gates of Hell', 'Shatter the Sky',
lambda state: state.has("Beat Gates of Hell", player)),
connect(world, player, names, 'Gates of Hell', 'All-In',
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)))
if get_option_value(world, player, "mission_order") == 0:
connect(world, player, names, 'Menu', 'Liberation Day'),
connect(world, player, names, 'Liberation Day', 'The Outlaws',
lambda state: state.has("Beat Liberation Day", player)),
connect(world, player, names, 'The Outlaws', 'Zero Hour',
lambda state: state.has("Beat The Outlaws", player)),
connect(world, player, names, 'Zero Hour', 'Evacuation',
lambda state: state.has("Beat Zero Hour", player)),
connect(world, player, names, 'Evacuation', 'Outbreak',
lambda state: state.has("Beat Evacuation", player)),
connect(world, player, names, "Outbreak", "Safe Haven",
lambda state: state._sc2wol_cleared_missions(world, player, 7) and
state.has("Beat Outbreak", player)),
connect(world, player, names, "Outbreak", "Haven's Fall",
lambda state: state._sc2wol_cleared_missions(world, player, 7) and
state.has("Beat Outbreak", player)),
connect(world, player, names, 'Zero Hour', 'Smash and Grab',
lambda state: state.has("Beat Zero Hour", player)),
connect(world, player, names, 'Smash and Grab', 'The Dig',
lambda state: state._sc2wol_cleared_missions(world, player, 8) and
state.has("Beat Smash and Grab", player)),
connect(world, player, names, 'The Dig', 'The Moebius Factor',
lambda state: state._sc2wol_cleared_missions(world, player, 11) and
state.has("Beat The Dig", player)),
connect(world, player, names, 'The Moebius Factor', 'Supernova',
lambda state: state._sc2wol_cleared_missions(world, player, 14) and
state.has("Beat The Moebius Factor", player)),
connect(world, player, names, 'Supernova', 'Maw of the Void',
lambda state: state.has("Beat Supernova", player)),
connect(world, player, names, 'Zero Hour', "Devil's Playground",
lambda state: state._sc2wol_cleared_missions(world, player, 4) and
state.has("Beat Zero Hour", player)),
connect(world, player, names, "Devil's Playground", 'Welcome to the Jungle',
lambda state: state.has("Beat Devil's Playground", player)),
connect(world, player, names, "Welcome to the Jungle", 'Breakout',
lambda state: state._sc2wol_cleared_missions(world, player, 8) and
state.has("Beat Welcome to the Jungle", player)),
connect(world, player, names, "Welcome to the Jungle", 'Ghost of a Chance',
lambda state: state._sc2wol_cleared_missions(world, player, 8) and
state.has("Beat Welcome to the Jungle", player)),
connect(world, player, names, "Zero Hour", 'The Great Train Robbery',
lambda state: state._sc2wol_cleared_missions(world, player, 6) and
state.has("Beat Zero Hour", player)),
connect(world, player, names, 'The Great Train Robbery', 'Cutthroat',
lambda state: state.has("Beat The Great Train Robbery", player)),
connect(world, player, names, 'Cutthroat', 'Engine of Destruction',
lambda state: state.has("Beat Cutthroat", player)),
connect(world, player, names, 'Engine of Destruction', 'Media Blitz',
lambda state: state.has("Beat Engine of Destruction", player)),
connect(world, player, names, 'Media Blitz', 'Piercing the Shroud',
lambda state: state.has("Beat Media Blitz", player)),
connect(world, player, names, 'The Dig', 'Whispers of Doom',
lambda state: state.has("Beat The Dig", player)),
connect(world, player, names, 'Whispers of Doom', 'A Sinister Turn',
lambda state: state.has("Beat Whispers of Doom", player)),
connect(world, player, names, 'A Sinister Turn', 'Echoes of the Future',
lambda state: state.has("Beat A Sinister Turn", player)),
connect(world, player, names, 'Echoes of the Future', 'In Utter Darkness',
lambda state: state.has("Beat Echoes of the Future", player)),
connect(world, player, names, 'Maw of the Void', 'Gates of Hell',
lambda state: state.has("Beat Maw of the Void", player)),
connect(world, player, names, 'Gates of Hell', 'Belly of the Beast',
lambda state: state.has("Beat Gates of Hell", player)),
connect(world, player, names, 'Gates of Hell', 'Shatter the Sky',
lambda state: state.has("Beat Gates of Hell", player)),
connect(world, player, names, 'Gates of Hell', 'All-In',
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]):

View File

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