diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index 836c8f6a85..2dbe5cdeb9 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -57,8 +57,8 @@ item_table = { "Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"), "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, parent_item="SCV"), "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, parent_item="SCV"), - "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler, parent_item="Building"), - "Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7, parent_item="Building"), + "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler), + "Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7), "Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, parent_item="Marine"), "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"), "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression, parent_item="Medic"), @@ -123,7 +123,7 @@ item_table = { "Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"), "Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"), "Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression), - "Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13), + "Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression), "Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler), "Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression), "Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16, classification=ItemClassification.filler), @@ -182,6 +182,22 @@ filler_items: typing.Tuple[str, ...] = ( '+15 Starting Vespene' ) +defense_ratings = { + "Siege Tank": 5, + "Maelstrom Rounds": 2, + "Planetary Fortress": 3, + # Bunker w/ Marine/Marauder: 3, + "Perdition Turret": 2, + "Missile Turret": 2, + "Vulture": 2 +} +zerg_defense_ratings = { + "Perdition Turret": 2, + # Bunker w/ Firebat + "Hive Mind Emulator": 3, + "Psi Disruptor": 3 +} + lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if data.code} # Map type to expected int diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index 358d0d227c..466c112c85 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -34,26 +34,33 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, 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)), + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 2 and + (logic_level > 0 or state._sc2wol_has_anti_air(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, 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)), + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 2), LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_competent_anti_air(world, player)), + (logic_level > 0 and state._sc2wol_has_anti_air(world, player) + or state._sc2wol_has_competent_anti_air(world, player))), LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401), 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("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, - lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), + lambda state: state._sc2wol_defense_rating(world, player, True) >= 4 and + (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)), + lambda state: state._sc2wol_defense_rating(world, player, True) >= 4 and + (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)), + lambda state: state._sc2wol_defense_rating(world, player, True) >= 4 and + (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_competent_anti_air(world, player)), @@ -68,38 +75,48 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L state._sc2wol_has_competent_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_competent_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 3), LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_competent_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 3), LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_competent_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 3), LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_competent_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 3), 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_competent_anti_air(world, player)), + (logic_level > 0 and state._sc2wol_has_anti_air(world, player) + or state._sc2wol_has_competent_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, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_has_common_unit(world, player) and + (logic_level > 0 and state._sc2wol_has_anti_air(world, player) + or state._sc2wol_has_competent_anti_air(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)), + (logic_level > 0 and state._sc2wol_has_anti_air(world, player) + or state._sc2wol_has_competent_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)), + lambda state: state._sc2wol_has_anti_air(world, player) and + state._sc2wol_defense_rating(world, player, False) >= 7), LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_defense_rating(world, player, False) >= 5), LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_defense_rating(world, player, False) >= 5), LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_defense_rating(world, player, False) >= 5), LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, - lambda state: state._sc2wol_has_air(world, player) and state._sc2wol_has_anti_air(world, player)), + lambda state: state._sc2wol_has_anti_air(world, player) and + (state._sc2wol_has_air(world, player) + or state.has_any({'Medivac', 'Hercules'}, player) + and state._sc2wol_has_common_unit(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)), LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, @@ -111,7 +128,10 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, - lambda state: state._sc2wol_has_air(world, player)), + lambda state: state._sc2wol_has_anti_air(world, player) and + (state._sc2wol_has_air(world, player) + or state.has_any({'Medivac', 'Hercules'}, player) + and state._sc2wol_has_common_unit(world, player))), LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), @@ -124,19 +144,20 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_survives_rip_field(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, - lambda state: state._sc2wol_survives_rip_field(world, player)), + lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)), LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, - lambda state: state._sc2wol_survives_rip_field(world, player)), + lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)), LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, lambda state: state._sc2wol_survives_rip_field(world, player)), LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, lambda state: state._sc2wol_survives_rip_field(world, player)), LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, - lambda state: state._sc2wol_has_anti_air(world, player) and ( - state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))), + lambda state: logic_level > 0 or + state._sc2wol_has_anti_air(world, player) and ( + 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)), + lambda state: logic_level > 0 or state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, lambda state: state._sc2wol_has_common_unit(world, player) and state._sc2wol_has_competent_anti_air(world, player)), @@ -163,7 +184,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702), LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703), LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_has_common_unit(world, player) and + (logic_level > 0 or state._sc2wol_has_anti_air)), 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, @@ -211,24 +233,28 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203), LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, 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), + LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301, + lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)), + LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302, + lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)), LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, lambda state: state._sc2wol_has_protoss_common_units(world, player)), LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, - lambda state: state._sc2wol_has_protoss_medium_units(world, player)), + lambda state: logic_level > 0 or 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)), + lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)), LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500), LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, lambda state: state._sc2wol_has_protoss_common_units(world, player)), LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, - lambda state: state._sc2wol_has_competent_comp(world, player)), + lambda state: state._sc2wol_has_competent_comp(world, player) and + state._sc2wol_defense_rating(world, player, True) > 5), LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, - lambda state: state._sc2wol_has_competent_comp(world, player)), + lambda state: state._sc2wol_has_competent_comp(world, player) and + state._sc2wol_defense_rating(world, player, True) > 5), 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), diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index 45b16dddc6..c28496ab57 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -1,34 +1,41 @@ from BaseClasses import MultiWorld from worlds.AutoWorld import LogicMixin from .Options import get_option_value -from .Items import get_basic_units +from .Items import get_basic_units, defense_ratings, zerg_defense_ratings class SC2WoLLogic(LogicMixin): def _sc2wol_has_common_unit(self, world: MultiWorld, player: int) -> bool: return self.has_any(get_basic_units(world, player), player) - def _sc2wol_has_manned_bunkers(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player) - def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or \ - self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player) + return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or get_option_value(world, player, 'required_tactics') > 0 \ + and self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player) def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Viking', 'Wraith'}, player) + return self.has('Viking', player) \ + or get_option_value(world, player, 'required_tactics') > 0 and self.has('Wraith', player) def _sc2wol_has_competent_anti_air(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(world, player) def _sc2wol_has_anti_air(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser'}, player) \ + return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Wraith'}, player) \ or self._sc2wol_has_competent_anti_air(world, player) \ - or get_option_value(world, player, 'required_tactics') > 0 and self.has_any('Ghost', 'Spectre') + or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player) - def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool: - return (self.has_any({'Siege Tank', 'Vulture', 'Planetary Fortress'}, player) or - self._sc2wol_has_manned_bunkers(world, player)) and self._sc2wol_has_anti_air(world, player) + def _sc2wol_defense_rating(self, world: MultiWorld, player: int, zerg_enemy: bool) -> bool: + defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player))) + if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player): + defense_score += 3 + if zerg_enemy: + defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player))) + if self.has('Firebat', player) and self.has('Bunker', player): + defense_score += 2 + # Advanced Tactics bumps defense rating requirements down by 2 + if get_option_value(world, player, 'required_tactics') > 0: + defense_score += 2 + return defense_score def _sc2wol_has_competent_comp(self, world: MultiWorld, player: int) -> bool: return (self.has('Marine', player) or self.has('Marauder', player) and @@ -38,12 +45,11 @@ class SC2WoLLogic(LogicMixin): self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(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) - or self.has('Marauder', player)) + return (self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player) or get_option_value(world, player, 'required_tactics') > 0 + and self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) + return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) or get_option_value(world, player, 'required_tactics') > 0 def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \ @@ -71,18 +77,18 @@ class SC2WoLLogic(LogicMixin): return get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player) def _sc2wol_final_mission_requirements(self, world: MultiWorld, player: int): + defense_rating = self._sc2wol_defense_rating(world, player, True) + beats_kerrigan = self.has_any({'Marine', 'Banshee', 'Ghost'}, player) or get_option_value(world, player, 'required_tactics') > 0 if get_option_value(world, player, 'all_in_map') == 0: # Ground - version_logic = sum( - self.has(item, player) for item in ['Planetary Fortress', 'Siege Tank', 'Psi Disruptor', 'Banshee', 'Battlecruiser', 'Maelstrom Rounds'] - ) + self._sc2wol_has_manned_bunkers(world, player) >= 4 + if self.has_any({'Battlecruiser', 'Banshee'}, player): + defense_rating += 3 + return defense_rating >= 12 and beats_kerrigan else: # Air - version_logic = self.has_any({'Viking', 'Battlecruiser'}, player) \ - and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player) - return self._sc2wol_has_heavy_defense(world, player) and version_logic + return defense_rating >= 8 and beats_kerrigan \ + and self.has_any({'Viking', 'Battlecruiser'}, player) \ + and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player) def _sc2wol_cleared_missions(self, world: MultiWorld, player: int, mission_count: int) -> bool: return self.has_group("Missions", player, mission_count) - - diff --git a/worlds/sc2wol/MissionTables.py b/worlds/sc2wol/MissionTables.py index 56e6e0d991..f69552b3c9 100644 --- a/worlds/sc2wol/MissionTables.py +++ b/worlds/sc2wol/MissionTables.py @@ -65,7 +65,7 @@ vanilla_shuffle_order = [ FillMission("all_in", [26, 27], "Char", completion_critical=True, or_requirements=True) ] -mini_shuffle_order = [ +mini_campaign_order = [ FillMission("no_build", [-1], "Mar Sara", completion_critical=True), FillMission("easy", [0], "Colonist"), FillMission("medium", [1], "Colonist"), @@ -102,13 +102,13 @@ grid_order = [ FillMission("medium", [1, 4], "Artifact", or_requirements=True), FillMission("hard", [2, 5, 10, 7], "Artifact", or_requirements=True), FillMission("hard", [3, 6, 11], "Artifact", or_requirements=True), - FillMission("medium", [4, 9], "Covert", or_requirements=True), - FillMission("hard", [5, 8, 10], "Covert", or_requirements=True), - FillMission("hard", [6, 9, 11], "Covert", or_requirements=True), + FillMission("medium", [4, 9, 12], "Covert", or_requirements=True), + FillMission("hard", [5, 8, 10, 13], "Covert", or_requirements=True), + FillMission("hard", [6, 9, 11, 14], "Covert", or_requirements=True), FillMission("hard", [7, 10], "Covert", or_requirements=True), FillMission("hard", [8, 13], "Rebellion", or_requirements=True), FillMission("hard", [9, 12, 14], "Rebellion", or_requirements=True), - FillMission("hard", [10, 13, 15], "Rebellion", or_requirements=True), + FillMission("hard", [10, 13], "Rebellion", or_requirements=True), FillMission("all_in", [11, 14], "Rebellion", or_requirements=True) ] @@ -139,7 +139,7 @@ blitz_order = [ FillMission("all_in", [0, 1], "Char", number=5, or_requirements=True) ] -mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_shuffle_order, grid_order, mini_grid_order, blitz_order, gauntlet_order] +mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_campaign_order, grid_order, mini_grid_order, blitz_order, gauntlet_order] vanilla_mission_req_table = { @@ -177,13 +177,16 @@ vanilla_mission_req_table = { lookup_id_to_mission: Dict[int, str] = { data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id} -starting_mission_locations = { +no_build_starting_mission_locations = { "Liberation Day": "Liberation Day: Victory", "Breakout": "Breakout: Victory", "Ghost of a Chance": "Ghost of a Chance: Victory", "Piercing the Shroud": "Piercing the Shroud: Victory", "Whispers of Doom": "Whispers of Doom: Victory", "Belly of the Beast": "Belly of the Beast: Victory", +} + +build_starting_mission_locations = { "Zero Hour": "Zero Hour: First Group Rescued", "Evacuation": "Evacuation: First Chysalis", "Devil's Playground": "Devil's Playground: Tosh's Miners" @@ -196,7 +199,22 @@ advanced_starting_mission_locations = { def get_starting_mission_locations(world: MultiWorld, player: int) -> set[str]: - if get_option_value(world, player, 'required_tactics') > 0: - return {**starting_mission_locations, **advanced_starting_mission_locations} + if get_option_value(world, player, 'shuffle_no_build') or get_option_value(world, player, 'mission_order') < 2: + # Always start with a no-build mission unless explicitly relegating them + # Vanilla and Vanilla Shuffled always start with a no-build even when relegated + return no_build_starting_mission_locations + elif get_option_value(world, player, 'required_tactics') > 0: + # Advanced Tactics/No Logic add more starting missions to the pool + return {**build_starting_mission_locations, **advanced_starting_mission_locations} else: - return starting_mission_locations + # Standard starting missions when relegate is on + return build_starting_mission_locations + + +alt_final_mission_locations = { + "Maw of the Void": "Maw of the Void: Victory", + "Engine of Destruction": "Engine of Destruction: Victory", + "Supernova": "Supernova: Victory", + "Gates of Hell": "Gates of Hell: Victory", + "Shatter the Sky": "Shatter the Sky: Victory" +} \ No newline at end of file diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py index afbb30a022..75f5492f3c 100644 --- a/worlds/sc2wol/Options.py +++ b/worlds/sc2wol/Options.py @@ -36,18 +36,18 @@ class AllInMap(Choice): 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. - Mini Shuffle: Shorter version of the campaign with randomized missions and optional branches. - Grid: A 4x4 grid of random missions. Start at the top left and forge a path towards All-In. - Mini Grid: A 3x3 version of Grid. - Blitz: 10 random missions that open up very quickly. - Gauntlet: Linear series of 7 random missions to complete the campaign.""" + """Determines the order the missions are played in. The last three mission orders end in a random mission. + Vanilla (29): Keeps the standard mission order and branching from the WoL Campaign. + Vanilla Shuffled (29): Keeps same branching paths from the WoL Campaign but randomizes the order of missions within. + Mini Campaign (15): Shorter version of the campaign with randomized missions and optional branches. + Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards All-In. + Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win. + Blitz (14): 14 random missions that open up very quickly. Complete the bottom-right mission to win. + Gauntlet (7): Linear series of 7 random missions to complete the campaign.""" display_name = "Mission Order" option_vanilla = 0 option_vanilla_shuffled = 1 - option_mini_shuffle = 2 + option_mini_campaign = 2 option_grid = 3 option_mini_grid = 4 option_blitz = 5 @@ -61,11 +61,11 @@ class ShuffleProtoss(DefaultOnToggle): display_name = "Shuffle Protoss Missions" -class RelegateNoBuildMissions(DefaultOnToggle): +class ShuffleNoBuild(DefaultOnToggle): """Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled. - If turned on with Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be placed at the end of optional routes. - If turned on with reduced mission settings, the 5 no-build missions will not appear.""" - display_name = "Relegate No-Build Missions" + If turned off with Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be placed at the end of optional routes. + If turned off with reduced mission settings, the 5 no-build missions will not appear.""" + display_name = "Shuffle No-Build Missions" class EarlyUnit(DefaultOnToggle): @@ -76,7 +76,7 @@ class EarlyUnit(DefaultOnToggle): class RequiredTactics(Choice): """Determines the maximum tactical difficulty of the seed (separate from mission difficulty). Higher settings increase randomness. Standard: All missions can be completed with good micro and macro. - Advanced: Completing missions may require relying on starting units and difficult-to-use units. + Advanced: Completing missions may require relying on starting units and micro-heavy units. No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!""" display_name = "Required Tactics" option_standard = 0 @@ -84,7 +84,7 @@ class RequiredTactics(Choice): option_no_logic = 2 -class UnitsAlwaysHaveUpgrades(Toggle): +class UnitsAlwaysHaveUpgrades(DefaultOnToggle): """If turned on, both upgrades will be present for each unit and structure in the seed. This usually results in fewer units.""" display_name = "Units Always Have Upgrades" @@ -115,7 +115,7 @@ sc2wol_options: Dict[str, Option] = { "all_in_map": AllInMap, "mission_order": MissionOrder, "shuffle_protoss": ShuffleProtoss, - "relegate_no_build": RelegateNoBuildMissions, + "shuffle_no_build": ShuffleNoBuild, "early_unit": EarlyUnit, "required_tactics": RequiredTactics, "units_always_have_upgrades": UnitsAlwaysHaveUpgrades, @@ -134,7 +134,7 @@ def get_option_value(world: MultiWorld, player: int, name: str) -> int: return int(option[player].value) -def get_option_set_value(world: MultiWorld, player: int, name: str) -> int: +def get_option_set_value(world: MultiWorld, player: int, name: str) -> set: option = getattr(world, name, None) if option is None: diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py index f6533959cf..3dcd36240a 100644 --- a/worlds/sc2wol/PoolFilter.py +++ b/worlds/sc2wol/PoolFilter.py @@ -2,7 +2,7 @@ from typing import Callable from BaseClasses import MultiWorld, ItemClassification, Item, Location from .Items import item_table from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\ - mission_orders, get_starting_mission_locations, MissionInfo, vanilla_mission_req_table + mission_orders, get_starting_mission_locations, MissionInfo, vanilla_mission_req_table, alt_final_mission_locations from .Options import get_option_value, get_option_set_value from .LogicMixin import SC2WoLLogic @@ -28,8 +28,7 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]: mission_order_type = get_option_value(world, player, "mission_order") shuffle_protoss = get_option_value(world, player, "shuffle_protoss") - relegate_no_build = get_option_value(world, player, "relegate_no_build") - excluded_missions: set[str] = get_option_set_value(world, player, "excluded_missions") + excluded_missions: set[str] = set(get_option_set_value(world, player, "excluded_missions")) invalid_mission_names = excluded_missions.difference(vanilla_mission_req_table.keys()) if invalid_mission_names: raise Exception("Error in locked_missions - the following are not valid mission names: " + ", ".join(invalid_mission_names)) @@ -41,31 +40,31 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]: "easy": easy_regions_list[:], "medium": medium_regions_list[:], "hard": hard_regions_list[:], - "all_in": "all_in" + "all_in": ["All-In"] } - mission_sets = [ - set(no_build_regions_list), - set(easy_regions_list), - set(medium_regions_list), - set(hard_regions_list) + mission_pools = [ + no_build_regions_list, + easy_regions_list, + medium_regions_list, + hard_regions_list ] - # Omitting No Build missions if relegating no-build - if relegate_no_build: - # The build missions in starting_mission_locations become the new "no build missions" - mission_sets[0] = set(get_starting_mission_locations(world, player).keys()) - mission_sets[0].difference_update(no_build_regions_list) - # Removing the new no-build missions from their original sets - for mission_set in mission_sets[1:]: - mission_set.difference_update(mission_sets[0]) # Omitting Protoss missions if not shuffling protoss if not shuffle_protoss: excluded_missions = excluded_missions.union(PROTOSS_REGIONS) - for mission_set in mission_sets: - mission_set.difference_update(excluded_missions) + # Replacing All-In on low mission counts + if mission_count < 14: + final_mission = world.random.choice([mission for mission in alt_final_mission_locations.keys() if mission not in excluded_missions]) + excluded_missions.add(final_mission) + else: + final_mission = 'All-In' + # Yaml settings determine which missions can be placed in the first slot + mission_pools[0] = [mission for mission in get_starting_mission_locations(world, player).keys() if mission not in excluded_missions] + # Removing the new no-build missions from their original sets + for i in range(1, len(mission_pools)): + mission_pools[i] = [mission for mission in mission_pools[i] if mission not in excluded_missions.union(mission_pools[0])] # Removing random missions from each difficulty set in a cycle set_cycle = 0 - mission_pools = [list(mission_set) for mission_set in mission_sets] current_count = sum(len(mission_pool) for mission_pool in mission_pools) if current_count < mission_count: raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.") @@ -86,7 +85,8 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]: "no_build": mission_pools[0], "easy": mission_pools[1], "medium": mission_pools[2], - "hard": mission_pools[3] + "hard": mission_pools[3], + "all_in": [final_mission] } @@ -109,28 +109,24 @@ class ValidInventory: def has_all(self, items: set[str], player: int): return all(item in self.logical_inventory for item in items) - def has_units_per_structure(self, min_units_per_structure) -> bool: - return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > min_units_per_structure and \ - len(FACTORY_UNITS.intersection(self.logical_inventory)) > min_units_per_structure and \ - len(STARPORT_UNITS.intersection(self.logical_inventory)) > min_units_per_structure + def has_units_per_structure(self) -> bool: + return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \ + len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \ + len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure def generate_reduced_inventory(self, inventory_size: int, mission_requirements: list[Callable]) -> list[Item]: """Attempts to generate a reduced inventory that can fulfill the mission requirements.""" inventory = list(self.item_pool) locked_items = list(self.locked_items) self.logical_inventory = { - item.name for item in inventory + locked_items + item.name for item in inventory + locked_items + self.existing_items if item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing) } requirements = mission_requirements cascade_keys = self.cascade_removal_map.keys() units_always_have_upgrades = get_option_value(self.world, self.player, "units_always_have_upgrades") - # Inventory restrictiveness based on number of missions with checks - mission_order_type = get_option_value(self.world, self.player, "mission_order") - mission_count = len(mission_orders[mission_order_type]) - 1 - min_units_per_structure = int(mission_count / 7) - if min_units_per_structure > 0: - requirements.append(lambda state: state.has_units_per_structure(min_units_per_structure)) + if self.min_units_per_structure > 0: + requirements.append(lambda state: state.has_units_per_structure()) def attempt_removal(item: Item) -> bool: # If item can be removed and has associated items, remove them as well @@ -181,7 +177,7 @@ class ValidInventory: self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player) self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player) self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player) - self._sc2wol_has_heavy_defense = lambda world, player: SC2WoLLogic._sc2wol_has_heavy_defense(self, world, player) + self._sc2wol_defense_rating = lambda world, player, zerg_enemy: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy) self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player) self._sc2wol_has_train_killers = lambda world, player: SC2WoLLogic._sc2wol_has_train_killers(self, world, player) self._sc2wol_able_to_rescue = lambda world, player: SC2WoLLogic._sc2wol_able_to_rescue(self, world, player) @@ -190,7 +186,6 @@ class ValidInventory: self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player) self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player) self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player) - self._sc2wol_has_manned_bunkers = lambda world, player: SC2WoLLogic._sc2wol_has_manned_bunkers(self, world, player) self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player) def __init__(self, world: MultiWorld, player: int, @@ -205,14 +200,19 @@ class ValidInventory: # Initial filter of item pool self.item_pool = [] item_quantities: dict[str, int] = dict() + # Inventory restrictiveness based on number of missions with checks + mission_order_type = get_option_value(self.world, self.player, "mission_order") + mission_count = len(mission_orders[mission_order_type]) - 1 + self.min_units_per_structure = int(mission_count / 7) + min_upgrades = 1 if mission_count < 10 else 2 for item in item_pool: item_info = item_table[item.name] if item_info.type == "Upgrade": - # All Upgrades are locked except for the final tier + # Locking upgrades based on mission duration if item.name not in item_quantities: item_quantities[item.name] = 0 item_quantities[item.name] += 1 - if item_quantities[item.name] < item_info.quantity: + if item_quantities[item.name] < min_upgrades: self.locked_items.append(item) else: self.item_pool.append(item) diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index ece55c738f..8e49291553 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -2,24 +2,37 @@ from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType from .Locations import LocationData from .Options import get_option_value -from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table +from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations from .PoolFilter import filter_missions import random -def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location]): +def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\ + -> tuple[dict[str, MissionInfo], int, str]: locations_per_region = get_locations_per_region(locations) mission_order_type = get_option_value(world, player, "mission_order") mission_order = mission_orders[mission_order_type] mission_pools = filter_missions(world, player) + final_mission = mission_pools['all_in'][0] used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool] - used_regions += ['All-In'] regions = [create_region(world, player, locations_per_region, location_cache, "Menu")] for region_name in used_regions: regions.append(create_region(world, player, locations_per_region, location_cache, region_name)) + # Changing the completion condition for alternate final missions into an event + if final_mission != 'All-In': + final_location = alt_final_mission_locations[final_mission] + # Final location should be near the end of the cache + for i in range(len(location_cache) - 1, -1, -1): + if location_cache[i].name == final_location: + location_cache[i].locked = True + location_cache[i].event = True + location_cache[i].address = None + break + else: + final_location = 'All-In: Victory' if __debug__: if mission_order_type in (0, 1): @@ -98,7 +111,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData 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 + return vanilla_mission_req_table, 29, final_location else: missions = [] @@ -108,8 +121,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData if mission is None: missions.append(None) elif mission.type == "all_in": - missions.append("All-In") - elif get_option_value(world, player, "relegate_no_build") and mission.relegate: + missions.append(final_mission) + elif mission.relegate and not get_option_value(world, player, "shuffle_no_build"): missions.append("no_build") else: missions.append(mission.type) @@ -117,11 +130,11 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData # Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled if get_option_value(world, player, "shuffle_protoss") == 0 and mission_order_type == 1: missions[22] = "A Sinister Turn" - medium_pool.remove("A Sinister Turn") + mission_pools['medium'].remove("A Sinister Turn") missions[23] = "Echoes of the Future" - medium_pool.remove("Echoes of the Future") + mission_pools['medium'].remove("Echoes of the Future") missions[24] = "In Utter Darkness" - hard_pool.remove("In Utter Darkness") + mission_pools['hard'].remove("In Utter Darkness") no_build_slots = [] easy_slots = [] @@ -191,7 +204,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData completion_critical=mission_order[i].completion_critical, or_requirements=mission_order[i].or_requirements)}) - return mission_req_table + final_mission_id = vanilla_mission_req_table[final_mission].id + return mission_req_table, final_mission_id, final_mission + ': Victory' def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index b10d5dfde1..0e341db96b 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -44,6 +44,8 @@ class SC2WoLWorld(World): locked_locations: typing.List[str] location_cache: typing.List[Location] mission_req_table = {} + final_mission_id: int + victory_item: str required_client_version = 0, 3, 5 def __init__(self, world: MultiWorld, player: int): @@ -60,8 +62,9 @@ class SC2WoLWorld(World): return StarcraftWoLItem(name, data.classification, data.code, self.player) def create_regions(self): - self.mission_req_table = create_regions(self.world, self.player, get_locations(self.world, self.player), - self.location_cache) + self.mission_req_table, self.final_mission_id, self.victory_item = 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) @@ -76,8 +79,7 @@ class SC2WoLWorld(World): def set_rules(self): setup_events(self.world, self.player, self.locked_locations, self.location_cache) - - self.world.completion_condition[self.player] = lambda state: state.has('All-In: Victory', self.player) + self.world.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player) def get_filler_item_name(self) -> str: return self.world.random.choice(filler_items) @@ -93,6 +95,7 @@ class SC2WoLWorld(World): slot_req_table[mission] = self.mission_req_table[mission]._asdict() slot_data["mission_req"] = slot_req_table + slot_data["final_mission"] = self.final_mission_id return slot_data @@ -186,7 +189,7 @@ def get_item_pool(world: MultiWorld, player: int, mission_req_table: dict[str, M else: pool.append(item) - existing_items = starter_items + [item.name for item in world.precollected_items[player]] + existing_items = starter_items + [item for item in world.precollected_items[player]] existing_names = [item.name for item in existing_items] # Removing upgrades for excluded items for item_name in excluded_items: