diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index 15942e696253..77c104b3a8f3 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -20,7 +20,7 @@ from Utils import init_logging, is_windows from worlds.sc2.Options import MissionOrder, KerriganPrimalStatus, kerrigan_unit_available, Kerriganless, GameSpeed, \ GenericUpgradeItems, GenericUpgradeResearch, ColorChoice, GenericUpgradeMissions, KerriganCheckLevelPackSize, KerriganChecksPerLevelPack, \ - LocationInclusion, MissionProgressLocations, OptionalBossLocations, ChallengeLocations, BonusLocations + LocationInclusion, MissionProgressLocations, OptionalBossLocations, ChallengeLocations, BonusLocations, EarlyUnit if __name__ == "__main__": init_logging("SC2Client", exception_logger="Client") @@ -278,6 +278,8 @@ class SC2Context(CommonContext): generic_upgrade_research = 0 generic_upgrade_items = 0 location_inclusions: typing.Dict[LocationType, LocationInclusion] = {} + plando_locations: typing.List[str] = [] + early_unit = 1 current_tooltip = None last_loc_list = None difficulty_override = -1 @@ -340,6 +342,8 @@ def on_package(self, cmd: str, args: dict) -> None: LocationType.CHALLENGE: args["slot_data"].get("challenge_locations", ChallengeLocations.default), LocationType.OPTIONAL_BOSS: args["slot_data"].get("optional_boss_locations", OptionalBossLocations.default), } + self.plando_locations = args["slot_data"].get("plando_locations", []) + self.early_unit = args["slot_data"].get("early_unit", EarlyUnit.default) self.build_location_to_mission_mapping() diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index 0df0e80025e1..094bcda78fb4 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -15,10 +15,10 @@ from CommonClient import CommonContext from worlds.sc2.Client import SC2Context, calc_unfinished_missions, parse_unlock -from worlds.sc2.MissionTables import lookup_id_to_mission, lookup_name_to_mission, SC2Mission +from worlds.sc2.MissionTables import lookup_id_to_mission, lookup_name_to_mission, SC2Mission, starting_mission_locations from worlds.sc2.Locations import LocationType, lookup_location_id_to_type from worlds.sc2.Options import LocationInclusion -from worlds.sc2 import SC2World +from worlds.sc2 import SC2World, get_first_mission, get_early_unit_location_name class HoverableButton(HoverBehavior, Button): @@ -82,6 +82,7 @@ class SC2Manager(GameManager): launching: Union[bool, int] = False # if int -> mission ID refresh_from_launching = True first_check = True + first_mission = "" ctx: SC2Context def __init__(self, ctx) -> None: @@ -117,6 +118,7 @@ def build_mission_table(self, dt) -> None: if self.ctx.mission_req_table: self.last_checked_locations = self.ctx.checked_locations.copy() self.first_check = False + self.first_mission = get_first_mission(self.ctx.mission_req_table) self.mission_id_to_button = {} @@ -159,7 +161,7 @@ def build_mission_table(self, dt) -> None: mission_obj: SC2Mission = lookup_name_to_mission[mission] mission_id: int = mission_obj.id mission_data = self.ctx.mission_req_table[campaign][mission] - remaining_locations, remaining_count = self.sort_unfinished_locations(mission) + remaining_locations, plando_locations, early_unit, remaining_count = self.sort_unfinished_locations(mission) # Map has uncollected locations if mission in unfinished_missions: if self.any_valuable_locations(remaining_locations): @@ -202,6 +204,11 @@ def build_mission_table(self, dt) -> None: else: tooltip += f"\n{self.get_location_type_title(loctype)}:\n- " tooltip += "\n- ".join(remaining_locations[loctype]) + if early_unit: + tooltip += f"\nEarly Unit:\n- {early_unit}" + if len(plando_locations) > 0: + tooltip += f"\nPlando:\n- " + tooltip += "\n- ".join(plando_locations) mission_button = MissionButton(text=text, size_hint_y=None, height=50) mission_button.tooltip_text = tooltip @@ -235,14 +242,29 @@ def mission_callback(self, button: MissionButton) -> None: def finish_launching(self, dt): self.launching = False - def sort_unfinished_locations(self, mission_name: str) -> (Dict[LocationType, List[str]], int): + def sort_unfinished_locations(self, mission_name: str) -> (Dict[LocationType, List[str]], List[str], str | None, int): locations: Dict[LocationType, List[str]] = {loctype: [] for loctype in LocationType} count = 0 for loc in self.ctx.locations_for_mission(mission_name): if loc in self.ctx.missing_locations: count += 1 locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names[loc]) - return locations, count + + early_unit = None + if self.ctx.early_unit and mission_name == self.first_mission: + early_unit = get_early_unit_location_name(mission_name) + for loctype in LocationType: + if early_unit in locations[loctype]: + locations[loctype].remove(early_unit) + + plando_locations = [] + for plando_loc in self.ctx.plando_locations: + for loctype in LocationType: + if plando_loc in locations[loctype]: + locations[loctype].remove(plando_loc) + plando_locations.append(plando_loc) + + return locations, plando_locations, early_unit, count def any_valuable_locations(self, locations: Dict[LocationType, List[str]]) -> bool: for loctype in LocationType: diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index 03d524a47266..1407290f5374 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -108,6 +108,7 @@ def fill_slot_data(self): if not isinstance(slot_req_table[campaign.id][mission]["required_world"][index], dict): slot_req_table[campaign.id][mission]["required_world"][index] = slot_req_table[campaign.id][mission]["required_world"][index]._asdict() + slot_data["plando_locations"] = get_plando_locations(self.multiworld, self.player) slot_data["mission_req"] = slot_req_table slot_data["final_mission"] = self.final_mission_id slot_data["version"] = 3 @@ -212,11 +213,7 @@ def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Se starter_items: List[Item] = [] non_local_items = multiworld.non_local_items[player].value if get_option_value(multiworld, player, "early_unit"): - # The first world should also be the starting world - campaigns = multiworld.worlds[player].mission_req_table.keys() - lowest_id = min([campaign.id for campaign in campaigns]) - first_campaign = [campaign for campaign in campaigns if campaign.id == lowest_id][0] - first_mission = list(multiworld.worlds[player].mission_req_table[first_campaign])[0] + first_mission = get_first_mission(multiworld.worlds[player].mission_req_table) first_race = lookup_name_to_mission[first_mission].race local_basic_unit = sorted(item for item in get_basic_units(multiworld, player, first_race) if item not in non_local_items and item not in excluded_items) @@ -225,12 +222,7 @@ def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Se if not local_basic_unit: raise Exception("Early Unit: At least one basic unit must be included") - if first_mission in starting_mission_locations: - first_location = starting_mission_locations[first_mission] - elif first_mission == "In Utter Darkness": - first_location = first_mission + ": Defeat" - else: - first_location = first_mission + ": Victory" + first_location = get_early_unit_location_name(first_mission) starter_items.append(assign_starter_item(multiworld, player, excluded_items, locked_locations, first_location, local_basic_unit)) @@ -258,6 +250,22 @@ def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Se return starter_items +def get_first_mission(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]]) -> str: + # The first world should also be the starting world + campaigns = mission_req_table.keys() + lowest_id = min([campaign.id for campaign in campaigns]) + first_campaign = [campaign for campaign in campaigns if campaign.id == lowest_id][0] + first_mission = list(mission_req_table[first_campaign])[0] + return first_mission + +def get_early_unit_location_name(first_mission: str) -> str: + if first_mission in starting_mission_locations: + first_location = starting_mission_locations[first_mission] + elif first_mission == "In Utter Darkness": + first_location = first_mission + ": Defeat" + else: + first_location = first_mission + ": Victory" + return first_location def assign_starter_item(multiworld: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str], location: str, item_list: Sequence[str]) -> Item: