diff --git a/Starcraft2Client.py b/Starcraft2Client.py index 692e4bc3..934f7171 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -57,13 +57,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) + request_available_missions(self.ctx.checked_locations, 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) + request_unfinished_missions(self.ctx.checked_locations, mission_req_table, self.ctx.ui) return True @@ -378,22 +378,23 @@ class MissionInfo(typing.NamedTuple): 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, []), - "The Outlaws": MissionInfo(2, 2, [1]), - "Zero Hour": MissionInfo(3, 4, [2]), + "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]), - "The Dig": MissionInfo(9, 4, [8], number=8), - "The Moebius Factor": MissionInfo(10, 9, [9], number=11), - "Supernova": MissionInfo(11, 5, [10], number=14), - "Maw of the Void": MissionInfo(12, 6, [11]), + "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), @@ -407,10 +408,10 @@ mission_req_table = { "A Sinister Turn": MissionInfo(23, 4, [22]), "Echoes of the Future": MissionInfo(24, 3, [23]), "In Utter Darkness": MissionInfo(25, 3, [24]), - "Gates of Hell": MissionInfo(26, 2, [12]), - "Belly of the Beast": MissionInfo(27, 4, [26]), - "Shatter the Sky": MissionInfo(28, 5, [26]), - "All-In": MissionInfo(29, -1, [27, 28], or_requirements=True) + "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: typing.Dict[int, str] = { @@ -431,16 +432,21 @@ def calc_objectives_completed(mission, missions_info, locations_done): return -1 -def request_unfinished_missions(locations_done, location_table): +def request_unfinished_missions(locations_done, location_table, ui): message = "Unfinished Missions: " unfinished_missions = calc_unfinished_missions(locations_done, location_table) - message += ", ".join(f"{mission}[{location_table[mission].id}] " + + 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) - sc2_logger.info(message) + if ui: + ui.log_panels['All'].on_message_markup(message) + ui.log_panels['Starcraft2'].on_message_markup(message) + else: + sc2_logger.info(message) def calc_unfinished_missions(locations_done, locations): @@ -469,13 +475,28 @@ 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 request_available_missions(locations_done, location_table): +def mark_critical(mission, location_table, ui): + """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]" + else: + return "*" + mission + "*" + else: + return mission + + +def request_available_missions(locations_done, location_table, ui): message = "Available Missions: " missions = calc_available_missions(locations_done, location_table) - message += ", ".join(f"{mission}[{location_table[mission].id}]" for mission in missions) + message += ", ".join(f"{mark_critical(mission,location_table, ui)}[{location_table[mission].id}]" for mission in missions) - sc2_logger.info(message) + if ui: + ui.log_panels['All'].on_message_markup(message) + ui.log_panels['Starcraft2'].on_message_markup(message) + else: + sc2_logger.info(message) def calc_available_missions(locations_done, locations): diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index 4397c95e..1e69cb2c 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -116,7 +116,7 @@ item_table = { "Automated Refinery": ItemData(604 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 4), "Command Center Reactor": ItemData(605 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 5), "Raven": ItemData(606 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 6), - "Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7), + "Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, progression=True), "Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8), "Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9), "Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10), diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index aa4f070b..402d2124 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -5,6 +5,7 @@ from BaseClasses import Location SC2WOL_LOC_ID_OFFSET = 1000 + class SC2WoLLocation(Location): game: str = "Starcraft2WoL" @@ -17,6 +18,7 @@ class LocationData(NamedTuple): def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]: + # Note: rules which are ended with or True are rules identified as needed later when restricted units is an option location_table: List[LocationData] = [ LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100), LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101), @@ -26,41 +28,84 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105), LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106), LocationData("Liberation Day", "Beat Liberation Day", None), - LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200), - LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201), - LocationData("The Outlaws", "Beat The Outlaws", None), - LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300), + LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("The Outlaws", "Beat The Outlaws", None, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, + lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301), - LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302), - LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303), - LocationData("Zero Hour", "Beat Zero Hour", None), - LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400), + LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Zero Hour", "Beat Zero Hour", None, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, + lambda state: state._sc2wol_has_mobile_anti_air(world, player)), LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401), - LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402), - LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403), - LocationData("Evacuation", "Beat Evacuation", None), - LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500), - LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501), - LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502), - LocationData("Outbreak", "Beat Outbreak", None), - LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600), - LocationData("Safe Haven", "Beat Safe Haven", None), - LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700), - LocationData("Haven's Fall", "Beat Haven's Fall", None), - LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800), + LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Evacuation", "Beat Evacuation", None, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_mobile_anti_air(world, player)), + LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, + lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), + LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, + lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), + LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, + lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), + LocationData("Outbreak", "Beat Outbreak", None, + lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), + LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_mobile_anti_air(world, player)), + LocationData("Safe Haven", "Beat Safe Haven", None, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_mobile_anti_air(world, player)), + LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_mobile_anti_air(world, player)), + LocationData("Haven's Fall", "Beat Haven's Fall", None, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_mobile_anti_air(world, player)), + LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_mobile_anti_air(world, player)), LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801), LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802), - LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803), - LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804), - LocationData("Smash and Grab", "Beat Smash and Grab", None), - LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900), - LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901), - LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902), - LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903), - LocationData("The Dig", "Beat The Dig", None), - LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1000), - LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001), - LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002), + LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_anti_air(world, player)), + LocationData("Smash and Grab", "Beat Smash and Grab", None, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_anti_air(world, player)), + LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, + 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 Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("The Dig", "Beat The Dig", None, + 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, + 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), + LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, + lambda state: state._sc2wol_has_air(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, lambda state: state._sc2wol_able_to_rescue(world, player) or True), LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, @@ -71,30 +116,58 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_able_to_rescue(world, player) or True), LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, lambda state: state._sc2wol_able_to_rescue(world, player) or True), - LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008), - LocationData("The Moebius Factor", "Beat The Moebius Factor", None), - LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100), + LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, + lambda state: state._sc2wol_has_air(world, player)), + LocationData("The Moebius Factor", "Beat The Moebius Factor", None, + lambda state: state._sc2wol_has_air(world, player)), + LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, + lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), - LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102), - LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103), - LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104), - LocationData("Supernova", "Beat Supernova", None), - LocationData("Maw of the Void", "Maw of the Void: Xel'Naga Vault", SC2WOL_LOC_ID_OFFSET + 1200), + LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, + lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, + 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, + 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), LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202), - LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203), - LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204), - LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205), - LocationData("Maw of the Void", "Beat Maw of the Void", None), - LocationData("Devil's Playground", "Devil's Playground: 8000 Minerals", SC2WOL_LOC_ID_OFFSET + 1300), + LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, + 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: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, + 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: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, + lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and + state._sc2wol_has_air(world, player)), + 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, + 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), - LocationData("Devil's Playground", "Beat Devil's Playground", None), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: 7 Canisters", SC2WOL_LOC_ID_OFFSET + 1400), + LocationData("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, + 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), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403), - LocationData("Welcome to the Jungle", "Beat Welcome to the Jungle", None), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, + 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: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_mobile_anti_air(world, player)), + 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: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501), LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502), @@ -106,36 +179,51 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604), LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605), LocationData("Ghost of a Chance", "Beat Ghost of a Chance", None), - LocationData("The Great Train Robbery", "The Great Train Robbery: 8 Trains", SC2WOL_LOC_ID_OFFSET + 1700, lambda state: state._sc2wol_has_train_killers(world, player)), + LocationData("The Great Train Robbery", "The Great Train Robbery: 8 Trains", SC2WOL_LOC_ID_OFFSET + 1700, + lambda state: state._sc2wol_has_train_killers(world, player)), LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701), LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702), LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703), LocationData("The Great Train Robbery", "Beat The Great Train Robbery", None, lambda state: state._sc2wol_has_train_killers(world, player)), - LocationData("Cutthroat", "Cutthroat: Orlan's Planetary", SC2WOL_LOC_ID_OFFSET + 1800), - LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801), - LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802), + LocationData("Cutthroat", "Cutthroat: Orlan's Planetary", 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)), + LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, + lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803), - LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804), - LocationData("Cutthroat", "Beat Cutthroat", None), + LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, + 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, lambda state: state._sc2wol_has_mobile_anti_air(world, player)), LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901), LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, - lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903), LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, - lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, - lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Beat Engine of Destruction", None, - lambda state: state._sc2wol_has_mobile_anti_air(world, player)), - LocationData("Media Blitz", "Media Blitz: Full Upload", SC2WOL_LOC_ID_OFFSET + 2000), - LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001), - LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002), - LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003), + 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, + 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)), + LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002, + lambda state: state._sc2wol_has_competent_comp(world, player)), + LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, + lambda state: state._sc2wol_has_competent_comp(world, player)), 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)), LocationData("Piercing the Shroud", "Piercing the Shroud: Facility Escape", SC2WOL_LOC_ID_OFFSET + 2100), LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101), LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102), @@ -156,31 +244,45 @@ 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: Overmind", 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), - LocationData("Echoes of the Future", "Beat Echoes of the Future", None), - LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2500), - LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501), + LocationData("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, + 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", "Beat In Utter Darkness", None), - LocationData("Gates of Hell", "Gates of Hell: Nydus Worms", SC2WOL_LOC_ID_OFFSET + 2600), - LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601), + LocationData("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, + 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: First Charge", SC2WOL_LOC_ID_OFFSET + 2701), LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702), LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703), LocationData("Belly of the Beast", "Beat Belly of the Beast", None), - LocationData("Shatter the Sky", "Shatter the Sky: Platform Destroyed", SC2WOL_LOC_ID_OFFSET + 2800), - LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801), - LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802), - LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803), - LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804), - LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805), - LocationData("Shatter the Sky", "Beat Shatter the Sky", None), + LocationData("Shatter the Sky", "Shatter the Sky: Platform Destroyed", 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)), + LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802, + lambda state: state._sc2wol_has_competent_comp(world, player)), + LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803, + lambda state: state._sc2wol_has_competent_comp(world, player)), + LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804, + lambda state: state._sc2wol_has_competent_comp(world, player)), + LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, + lambda state: state._sc2wol_has_competent_comp(world, player)), + LocationData("Shatter the Sky", "Beat Shatter the Sky", None, + lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("All-In", "All-In: Victory", None) ] return tuple(location_table) - diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index 72147372..7e2fc2f0 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -12,9 +12,6 @@ class SC2WoLLogic(LogicMixin): def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Viking', 'Wraith', 'Medivac', 'Banshee', 'Hercules'}, player) - def _sc2wol_has_battlecruiser(self, world: MultiWorld, player: int) -> bool: - return self.has('Battlecruiser', player) - def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Viking', 'Wraith'}, player) @@ -29,6 +26,12 @@ class SC2WoLLogic(LogicMixin): self.has('Bunker', player) and self._sc2wol_has_bunker_unit(world, player)) and \ self._sc2wol_has_anti_air(world, player) + def _sc2wol_has_competent_comp(self, world: MultiWorld, player: int) -> bool: + return (self.has('Marine', player) or self.has('Marauder', player) and + self._sc2wol_has_mobile_anti_air(world, player)) and self.has_any({'Medivac', 'Medic'}, player) or \ + self.has('Thor', player) or self.has("Banshee", player) and self._sc2wol_has_mobile_anti_air(world, player) or \ + self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(world, player) + def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool: return (self.has_any({'Siege Tank', 'Diamondback'}, player) or self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index 0f0c408e..e0d8415b 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -48,54 +48,71 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, 'Menu', 'Liberation Day'), connect(world, player, names, 'Liberation Day', 'The Outlaws', - lambda state: state._sc2wol_has_common_unit(world, player)), - connect(world, player, names, 'The Outlaws', 'Zero Hour'), + 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._sc2wol_has_anti_air(world, player)), - connect(world, player, names, 'Evacuation', 'Outbreak'), + 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_has_mobile_anti_air(world, player) and - state._sc2wol_cleared_missions(world, player, 7)), + 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_has_mobile_anti_air(world, player) and - state._sc2wol_cleared_missions(world, player, 7)), + 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._sc2wol_has_anti_air(world, player)), + 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._sc2wol_has_heavy_defense(world, player)), + 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._sc2wol_has_air(world, player)), + state.has("Beat The Dig", player)), connect(world, player, names, 'The Moebius Factor', 'Supernova', - lambda state: state._sc2wol_cleared_missions(world, player, 14)), - connect(world, player, names, 'Supernova', 'Maw of the Void'), + 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)), - connect(world, player, names, "Devil's Playground", 'Welcome to the Jungle'), + 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)), + 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)), + 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)), + 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 The Great Train Robbery", player)), + 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'), - connect(world, player, names, 'The Dig', 'Whispers of Doom',), - connect(world, player, names, 'Whispers of Doom', 'A Sinister Turn'), + 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'), - connect(world, player, names, 'Maw of the Void', 'Gates of Hell'), - connect(world, player, names, 'Gates of Hell', 'Belly of the Beast'), - connect(world, player, names, 'Gates of Hell', 'Shatter the Sky'), + connect(world, player, names, '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) or state.has('Beat Shatter the Sky', 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))) def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): @@ -105,10 +122,12 @@ def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: existingRegions.add(region.name) if (regionNames - existingRegions): - raise Exception("Starcraft: the following regions are used in locations: {}, but no such region exists".format(regionNames - existingRegions)) + raise Exception("Starcraft: the following regions are used in locations: {}, but no such region exists".format( + regionNames - existingRegions)) -def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location: +def create_location(player: int, location_data: LocationData, region: Region, + location_cache: List[Location]) -> Location: location = Location(player, location_data.name, location_data.code, region) location.access_rule = location_data.rule @@ -121,7 +140,8 @@ def create_location(player: int, location_data: LocationData, region: Region, lo return location -def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], location_cache: List[Location], name: str) -> Region: +def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], + location_cache: List[Location], name: str) -> Region: region = Region(name, RegionType.Generic, name, player) region.world = world @@ -133,7 +153,8 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str return region -def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str, rule: Optional[Callable] = None): +def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str, + rule: Optional[Callable] = None): sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) @@ -154,7 +175,7 @@ def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]: - per_region: Dict[str, List[LocationData]] = {} + per_region: Dict[str, List[LocationData]] = {} for location in locations: per_region.setdefault(location.region, []).append(location)