diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index f59a86beb0da..cd58fc1fa650 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -50,14 +50,18 @@ loop = asyncio.get_event_loop_policy().new_event_loop() nest_asyncio.apply(loop) -max_bonus: int = 13 -victory_modulo: int = 100 +MAX_BONUS: int = 28 +VICTORY_MODULO: int = 100 # GitHub repo where the Map/mod data is hosted for /download_data command DATA_REPO_OWNER = "Ziktofel" DATA_REPO_NAME = "Archipelago-SC2-data" DATA_API_VERSION = "API3" +# Bot controller +CONTROLLER_HEALTH: int = 38281 +CONTROLLER2_HEALTH: int = 38282 + # Data version file path. # This file is used to tell if the downloaded data are outdated @@ -323,6 +327,7 @@ class SC2Context(CommonContext): minerals_per_item = 15 vespene_per_item = 15 starting_supply_per_item = 2 + nova_covert_ops_only = False def __init__(self, *args, **kwargs) -> None: super(SC2Context, self).__init__(*args, **kwargs) @@ -386,6 +391,7 @@ def on_package(self, cmd: str, args: dict) -> None: self.minerals_per_item = args["slot_data"].get("minerals_per_item", 15) self.vespene_per_item = args["slot_data"].get("vespene_per_item", 15) self.starting_supply_per_item = args["slot_data"].get("starting_supply_per_item", 2) + self.nova_covert_ops_only = args["slot_data"].get("nova_covert_ops_only", False) if self.required_tactics == RequiredTactics.option_no_logic: # Locking Grant Story Tech if no logic @@ -488,8 +494,8 @@ def build_location_to_mission_mapping(self) -> None: for loc in self.server_locations: offset = SC2WOL_LOC_ID_OFFSET if loc < SC2HOTS_LOC_ID_OFFSET \ - else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * victory_modulo) - mission_id, objective = divmod(loc - offset, victory_modulo) + else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * VICTORY_MODULO) + mission_id, objective = divmod(loc - offset, VICTORY_MODULO) mission_id_to_location_ids[mission_id].add(objective) self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in mission_id_to_location_ids.items()} @@ -499,7 +505,7 @@ def locations_for_mission(self, mission_name: str): mission_id: int = mission.id objectives = self.mission_id_to_location_ids[mission_id] for objective in objectives: - yield get_location_offset(mission_id) + mission_id * victory_modulo + objective + yield get_location_offset(mission_id) + mission_id * VICTORY_MODULO + objective class CompatItemHolder(typing.NamedTuple): @@ -625,7 +631,7 @@ def calculate_items(ctx: SC2Context) -> typing.Dict[SC2Race, typing.List[int]]: 3 * num_missions // 100 ] upgrade_count = 0 - completed = len([id for id in ctx.mission_id_to_location_ids if get_location_offset(id) + victory_modulo * id in ctx.checked_locations]) + completed = len([id for id in ctx.mission_id_to_location_ids if get_location_offset(id) + VICTORY_MODULO * id in ctx.checked_locations]) for amount in amounts: if completed >= amount: upgrade_count += 1 @@ -719,7 +725,7 @@ def kerrigan_primal(ctx: SC2Context, items: typing.Dict[SC2Race, typing.List[int return items[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]] >= 35 elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_half_completion: total_missions = len(ctx.mission_id_to_location_ids) - completed = len([(mission_id * victory_modulo + get_location_offset(mission_id)) in ctx.checked_locations + completed = len([(mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations for mission_id in ctx.mission_id_to_location_ids]) return completed >= (total_missions / 2) elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_item: @@ -752,7 +758,7 @@ def __init__(self, ctx: SC2Context, mission_id): self.ctx = ctx self.ctx.last_bot = self self.mission_id = mission_id - self.boni = [False for _ in range(max_bonus)] + self.boni = [False for _ in range(MAX_BONUS)] super(ArchipelagoBot, self).__init__() @@ -776,7 +782,7 @@ async def on_step(self, iteration: int): game_speed = self.ctx.game_speed_override else: game_speed = self.ctx.game_speed - await self.chat_send("?SetOptions {} {} {} {} {} {} {} {} {} {}".format( + await self.chat_send("?SetOptions {} {} {} {} {} {} {} {} {} {} {} {}".format( difficulty, self.ctx.generic_upgrade_research, self.ctx.all_in_choice, @@ -786,7 +792,9 @@ async def on_step(self, iteration: int): kerrigan_options, self.ctx.grant_story_tech, self.ctx.take_over_ai_allies, - soa_options + soa_options, + self.ctx.mission_order, + 1 if self.ctx.nova_covert_ops_only else 0 )) await self.chat_send("?GiveResources {} {} {}".format( start_items[SC2Race.ANY][0], @@ -809,10 +817,16 @@ async def on_step(self, iteration: int): self.ctx.announcements.task_done() # Archipelago reads the health + controller1_state = 0 + controller2_state = 0 for unit in self.all_own_units(): - if unit.health_max == 38281: - game_state = int(38281 - unit.health) + if unit.health_max == CONTROLLER_HEALTH: + controller1_state = int(CONTROLLER_HEALTH - unit.health) + self.can_read_game = True + elif unit.health_max == CONTROLLER2_HEALTH: + controller2_state = int(CONTROLLER2_HEALTH - unit.health) self.can_read_game = True + game_state = controller1_state + (controller2_state << 15) if iteration == 160 and not game_state & 1: await self.chat_send("?SendMessage Warning: Archipelago unable to connect or has lost connection to " + @@ -836,7 +850,7 @@ async def on_step(self, iteration: int): print("Mission Completed") await self.ctx.send_msgs( [{"cmd": 'LocationChecks', - "locations": [get_location_offset(self.mission_id) + victory_modulo * self.mission_id]}]) + "locations": [get_location_offset(self.mission_id) + VICTORY_MODULO * self.mission_id]}]) self.mission_completed = True else: print("Game Complete") @@ -849,7 +863,7 @@ async def on_step(self, iteration: int): checks = len(self.ctx.checked_locations) await self.ctx.send_msgs( [{"cmd": 'LocationChecks', - "locations": [get_location_offset(self.mission_id) + victory_modulo * self.mission_id + x + 1]}]) + "locations": [get_location_offset(self.mission_id) + VICTORY_MODULO * self.mission_id + x + 1]}]) self.boni[x] = True # Kerrigan level needs manual updating if the check's receiver isn't the local player if self.ctx.levels_per_check > 0 and self.last_received_update == len(self.ctx.items_received): @@ -865,10 +879,10 @@ async def on_step(self, iteration: int): async def updateTerranTech(self, current_items): terran_items = current_items[SC2Race.TERRAN] - await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {} {} {}".format( + await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {} {} {} {} {}".format( terran_items[0], terran_items[1], terran_items[2], terran_items[3], terran_items[4], terran_items[5], terran_items[6], terran_items[7], terran_items[8], terran_items[9], terran_items[10], - terran_items[11])) + terran_items[11], terran_items[12], terran_items[13])) async def updateZergTech(self, current_items): zerg_items = current_items[SC2Race.ZERG] @@ -1013,7 +1027,7 @@ def calc_available_missions(ctx: SC2Context, unlocks: typing.Optional[dict] = No # Get number of missions completed for loc in ctx.checked_locations: - if loc % victory_modulo == 0: + if loc % VICTORY_MODULO == 0: missions_complete += 1 for campaign in ctx.mission_req_table: @@ -1068,7 +1082,7 @@ def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete # Check if required mission has been completed mission_id = ctx.mission_req_table[parsed_req_mission.campaign][ list(ctx.mission_req_table[parsed_req_mission.campaign])[parsed_req_mission.connect_to - 1]].mission.id - if not (mission_id * victory_modulo + get_location_offset(mission_id)) in ctx.checked_locations: + if not (mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations: if not ctx.mission_req_table[campaign][mission_name].or_requirements: return False else: @@ -1331,7 +1345,7 @@ def is_mod_update_available(owner: str, repo: str, api_version: str, metadata: s def get_location_offset(mission_id): return SC2WOL_LOC_ID_OFFSET if mission_id <= SC2Mission.ALL_IN.id \ - else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * victory_modulo) + else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * VICTORY_MODULO) def launch(): diff --git a/worlds/sc2/ItemNames.py b/worlds/sc2/ItemNames.py index c7b62471c9b3..838f381b59e1 100644 --- a/worlds/sc2/ItemNames.py +++ b/worlds/sc2/ItemNames.py @@ -115,6 +115,8 @@ BATTLECRUISER_MISSILE_PODS = "Missile Pods (Battlecruiser)" BATTLECRUISER_OPTIMIZED_LOGISTICS = "Optimized Logistics (Battlecruiser)" BATTLECRUISER_TACTICAL_JUMP = "Tactical Jump (Battlecruiser)" +BATTLECRUISER_BEHEMOTH_PLATING = "Behemoth Plating (Battlecruiser)" +BATTLECRUISER_COVERT_OPS_ENGINES = "Covert Ops Engines (Battlecruiser)" BUNKER_NEOSTEEL_BUNKER = "Neosteel Bunker (Bunker)" BUNKER_PROJECTILE_ACCELERATOR = "Projectile Accelerator (Bunker)" BUNKER_SHRIKE_TURRET = "Shrike Turret (Bunker)" @@ -273,6 +275,27 @@ WRAITH_INTERNAL_TECH_MODULE = "Internal Tech Module (Wraith)" WRAITH_RESOURCE_EFFICIENCY = "Resource Efficiency (Wraith)" +# Nova +NOVA_GHOST_VISOR = "Ghost Visor (Nova Equipment)" +NOVA_RANGEFINDER_OCULUS = "Rangefinder Oculus (Nova Equipment)" +NOVA_DOMINATION = "Domination (Nova Ability)" +NOVA_BLINK = "Blink (Nova Ability)" +NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE = "Progressive Stealth Suit Module (Nova Suit Module)" +NOVA_ENERGY_SUIT_MODULE = "Energy Suit Module (Nova Suit Module)" +NOVA_ARMORED_SUIT_MODULE = "Armored Suit Module (Nova Suit Module)" +NOVA_JUMP_SUIT_MODULE = "Jump Suit Module (Nova Suit Module)" +NOVA_C20A_CANISTER_RIFLE = "C20A Canister Rifle (Nova Weapon)" +NOVA_HELLFIRE_SHOTGUN = "Hellfire Shotgun (Nova Weapon)" +NOVA_PLASMA_RIFLE = "Plasma Rifle (Nova Weapon)" +NOVA_MONOMOLECULAR_BLADE = "Monomolecular Blade (Nova Weapon)" +NOVA_BLAZEFIRE_GUNBLADE = "Blazefire Gunblade (Nova Weapon)" +NOVA_STIM_INFUSION = "Stim Infusion (Nova Gadget)" +NOVA_PULSE_GRENADES = "Pulse Grenades (Nova Gadget)" +NOVA_FLASHBANG_GRENADES = "Flashbang Grenades (Nova Gadget)" +NOVA_IONIC_FORCE_FIELD = "Ionic Force Field (Nova Gadget)" +NOVA_HOLO_DECOY = "Holo Decoy (Nova Gadget)" +NOVA_NUKE = "Tac Nuke Strike (Nova Ability)" + # Zerg Units ZERGLING = "Zergling" SWARM_QUEEN = "Swarm Queen" diff --git a/worlds/sc2/Items.py b/worlds/sc2/Items.py index 363bf53f6b37..bf2dda535a1e 100644 --- a/worlds/sc2/Items.py +++ b/worlds/sc2/Items.py @@ -192,7 +192,7 @@ def get_full_item_list(): )), ItemNames.HERC: ItemData(22 + SC2WOL_ITEM_ID_OFFSET, "Unit", 26, SC2Race.TERRAN, - classification=ItemClassification.useful, origin={"ext"}, + classification=ItemClassification.progression, origin={"ext"}, description=inspect.cleandoc( """ Front-line infantry. Can use Grapple. @@ -512,7 +512,7 @@ def get_full_item_list(): description="Reapers regenerate life while out of combat."), ItemNames.HELLION_HELLBAT_ASPECT: ItemData(255 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21, SC2Race.TERRAN, - parent_item=ItemNames.HELLION, origin={"nco"}, + classification=ItemClassification.progression, parent_item=ItemNames.HELLION, origin={"nco"}, description="Allows Hellions to transform into Hellbats."), ItemNames.HELLION_SMART_SERVOS: ItemData(256 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, SC2Race.TERRAN, @@ -549,7 +549,7 @@ def get_full_item_list(): description="Increases Spider mine damage."), ItemNames.GOLIATH_JUMP_JETS: ItemData(263 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 28, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.GOLIATH, origin={"nco"}, + classification=ItemClassification.progression, parent_item=ItemNames.GOLIATH, origin={"nco"}, description="Allows Goliaths to jump up and down cliffs."), ItemNames.GOLIATH_OPTIMIZED_LOGISTICS: ItemData(264 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 29, SC2Race.TERRAN, @@ -574,7 +574,7 @@ def get_full_item_list(): description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Diamondback")), ItemNames.SIEGE_TANK_JUMP_JETS: ItemData(268 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 3, SC2Race.TERRAN, - parent_item=ItemNames.SIEGE_TANK, origin={"nco"}, + classification=ItemClassification.progression, parent_item=ItemNames.SIEGE_TANK, origin={"nco"}, description=inspect.cleandoc( """ Repositions Siege Tank to a target location. @@ -1085,6 +1085,14 @@ def get_full_item_list(): ItemData(392 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 10, SC2Race.TERRAN, parent_item=ItemNames.PREDATOR, origin={"ext"}, description="Predators can use an attack that jumps between targets."), + ItemNames.BATTLECRUISER_BEHEMOTH_PLATING: + ItemData(393 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 11, SC2Race.TERRAN, + parent_item=ItemNames.BATTLECRUISER, origin={"ext"}, + description="Increases Battlecruiser armor by 2."), + ItemNames.BATTLECRUISER_COVERT_OPS_ENGINES: + ItemData(394 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 12, SC2Race.TERRAN, + parent_item=ItemNames.BATTLECRUISER, origin={"nco"}, + description="Increases Battlecruiser movement speed."), #Buildings ItemNames.BUNKER: @@ -1264,7 +1272,26 @@ def get_full_item_list(): # This item is used to "remove" location from the game. Never placed unless plando'd ItemNames.NOTHING: ItemData(803 + SC2WOL_ITEM_ID_OFFSET, "Nothing Group", 2, SC2Race.ANY, quantity=0, classification=ItemClassification.trap), - # ItemNames.KEYSTONE_PIECE: ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing) + # Nova gear + ItemNames.NOVA_GHOST_VISOR: ItemData(900 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 0, SC2Race.TERRAN, origin={"nco"}), + ItemNames.NOVA_RANGEFINDER_OCULUS: ItemData(901 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 1, SC2Race.TERRAN, origin={"nco"}), + ItemNames.NOVA_DOMINATION: ItemData(902 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 2, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_BLINK: ItemData(903 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 3, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: ItemData(904 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade 2", 0, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_ENERGY_SUIT_MODULE: ItemData(905 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 4, SC2Race.TERRAN, origin={"nco"}), + ItemNames.NOVA_ARMORED_SUIT_MODULE: ItemData(906 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 5, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_JUMP_SUIT_MODULE: ItemData(907 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 6, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_C20A_CANISTER_RIFLE: ItemData(908 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 7, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_HELLFIRE_SHOTGUN: ItemData(909 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 8, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_PLASMA_RIFLE: ItemData(910 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 9, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_MONOMOLECULAR_BLADE: ItemData(911 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 10, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_BLAZEFIRE_GUNBLADE: ItemData(912 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 11, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_STIM_INFUSION: ItemData(913 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 12, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_PULSE_GRENADES: ItemData(914 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 13, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_FLASHBANG_GRENADES: ItemData(915 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 14, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_IONIC_FORCE_FIELD: ItemData(916 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 15, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_HOLO_DECOY: ItemData(917 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 16, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), + ItemNames.NOVA_NUKE: ItemData(918 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 17, SC2Race.TERRAN, origin={"nco"}, classification=ItemClassification.progression), # HotS ItemNames.ZERGLING: ItemData(0 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 0, SC2Race.ZERG, classification=ItemClassification.progression, origin={"hots"}), @@ -1756,9 +1783,10 @@ def get_basic_units(multiworld: MultiWorld, player: int, race: SC2Race) -> typin # Mercenaries (All races) *[item_name for item_name, item_data in get_full_item_list().items() if item_data.type == "Mercenary"], - # Kerrigan levels, abilities and generally useful stuff + # Kerrigan and Nova levels, abilities and generally useful stuff *[item_name for item_name, item_data in get_full_item_list().items() - if item_data.type in ("Level", "Ability", "Evolution Pit")], + if item_data.type in ("Level", "Ability", "Evolution Pit", "Nova Gear")], + ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, # Zerg static defenses ItemNames.SPORE_CRAWLER, ItemNames.SPINE_CRAWLER, @@ -1800,6 +1828,7 @@ def get_basic_units(multiworld: MultiWorld, player: int, race: SC2Race) -> typin filler_items: typing.Tuple[str, ...] = ( ItemNames.STARTING_MINERALS, ItemNames.STARTING_VESPENE, + ItemNames.STARTING_SUPPLY, ) # Defense rating table @@ -1826,7 +1855,6 @@ def get_basic_units(multiworld: MultiWorld, player: int, race: SC2Race) -> typin } air_defense_ratings = { ItemNames.MISSILE_TURRET: 2, - ItemNames.VALKYRIE: 2 } spider_mine_sources = { @@ -1896,6 +1924,12 @@ def get_basic_units(multiworld: MultiWorld, player: int, race: SC2Race) -> typin ItemNames.OVERWATCH, } +nova_equimpent = { + *[item_name for item_name, item_data in get_full_item_list().items() + if item_data.type == "Nova Gear"], + ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE +} + # 'number' values of upgrades for upgrade bundle items upgrade_numbers = [ # Terran @@ -1994,6 +2028,8 @@ def get_basic_units(multiworld: MultiWorld, player: int, race: SC2Race) -> typin "Unit": 9, "Building": 10, "Mercenary": 11, + "Nova Gear": 12, + "Progressive Upgrade 2": 13, }, SC2Race.ZERG: { "Ability": 0, diff --git a/worlds/sc2/Locations.py b/worlds/sc2/Locations.py index 6baed7ff85a8..593c5ca78ee1 100644 --- a/worlds/sc2/Locations.py +++ b/worlds/sc2/Locations.py @@ -9,8 +9,9 @@ from BaseClasses import Location SC2WOL_LOC_ID_OFFSET = 1000 -SC2HOTS_LOC_ID_OFFSET = 20000000 # Avoid clashes with The Legend of Zelda +SC2HOTS_LOC_ID_OFFSET = 20000000 # Avoid clashes with The Legend of Zelda SC2LOTV_LOC_ID_OFFSET = SC2HOTS_LOC_ID_OFFSET + 2000 +SC2NCO_LOC_ID_OFFSET = SC2LOTV_LOC_ID_OFFSET + 2500 class SC2Location(Location): @@ -226,14 +227,23 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu (adv_tactics and logic.terran_basic_anti_air(state) or logic.terran_competent_anti_air(state))), LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, LocationType.VICTORY, - lambda state: logic.terran_basic_anti_air(state) and - logic.terran_defense_rating(state, False) >= 7), + lambda state: logic.terran_basic_anti_air(state) + and logic.terran_defense_rating(state, False, True) >= 8 + and logic.terran_defense_rating(state, False, False) >= 6 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics)), LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, LocationType.VANILLA, - lambda state: logic.terran_defense_rating(state, False) >= 5), + lambda state: logic.terran_defense_rating(state, False, False) >= 6 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics)), LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, LocationType.VANILLA, - lambda state: logic.terran_defense_rating(state, False) >= 5), + lambda state: logic.terran_defense_rating(state, False, False) >= 6 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics)), LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, LocationType.VANILLA, - lambda state: logic.terran_defense_rating(state, False) >= 5), + lambda state: logic.terran_defense_rating(state, False, False) >= 6 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics)), LocationData("The Dig", "The Dig: Moebius Base", SC2WOL_LOC_ID_OFFSET + 904, LocationType.EXTRA, lambda state: logic.marine_medic_upgrade(state) or adv_tactics), LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, LocationType.VICTORY, @@ -1160,6 +1170,182 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu LocationData("The Essence of Eternity", "The Essence of Eternity: Void Trashers", SC2LOTV_LOC_ID_OFFSET + 2401, LocationType.EXTRA), LocationData("Amon's Fall", "Amon's Fall: Victory", SC2LOTV_LOC_ID_OFFSET + 2500, LocationType.VICTORY, lambda state: logic.amons_fall_requirement(state)), + + # Nova Covert Ops + LocationData("The Escape", "The Escape: Victory", SC2NCO_LOC_ID_OFFSET + 100, LocationType.VICTORY, + lambda state: logic.the_escape_requirement(state)), + LocationData("The Escape", "The Escape: Rifle", SC2NCO_LOC_ID_OFFSET + 101, LocationType.VANILLA, + lambda state: logic.the_escape_first_stage_requirement(state)), + LocationData("The Escape", "The Escape: Grenades", SC2NCO_LOC_ID_OFFSET + 102, LocationType.VANILLA, + lambda state: logic.the_escape_first_stage_requirement(state)), + LocationData("The Escape", "The Escape: Agent Delta", SC2NCO_LOC_ID_OFFSET + 103, LocationType.VANILLA, + lambda state: logic.the_escape_requirement(state)), + LocationData("The Escape", "The Escape: Agent Pierce", SC2NCO_LOC_ID_OFFSET + 104, LocationType.VANILLA, + lambda state: logic.the_escape_requirement(state)), + LocationData("The Escape", "The Escape: Agent Stone", SC2NCO_LOC_ID_OFFSET + 105, LocationType.VANILLA, + lambda state: logic.the_escape_requirement(state)), + LocationData("Sudden Strike", "Sudden Strike: Victory", SC2NCO_LOC_ID_OFFSET + 200, LocationType.VICTORY, + lambda state: logic.sudden_strike_can_reach_objectives(state)), + LocationData("Sudden Strike", "Sudden Strike: Research Center", SC2NCO_LOC_ID_OFFSET + 201, LocationType.VANILLA, + lambda state: logic.sudden_strike_can_reach_objectives(state)), + LocationData("Sudden Strike", "Sudden Strike: Weaponry Labs", SC2NCO_LOC_ID_OFFSET + 202, LocationType.VANILLA, + lambda state: logic.sudden_strike_requirement(state)), + LocationData("Sudden Strike", "Sudden Strike: Brutalisk", SC2NCO_LOC_ID_OFFSET + 203, LocationType.EXTRA, + lambda state: logic.sudden_strike_requirement(state)), + LocationData("Enemy Intelligence", "Enemy Intelligence: Victory", SC2NCO_LOC_ID_OFFSET + 300, LocationType.VICTORY, + lambda state: logic.enemy_intelligence_third_stage_requirement(state)), + LocationData("Enemy Intelligence", "Enemy Intelligence: West Garrison", SC2NCO_LOC_ID_OFFSET + 301, LocationType.EXTRA, + lambda state: logic.enemy_intelligence_first_stage_requirement(state)), + LocationData("Enemy Intelligence", "Enemy Intelligence: Close Garrison", SC2NCO_LOC_ID_OFFSET + 302, LocationType.EXTRA, + lambda state: logic.enemy_intelligence_first_stage_requirement(state)), + LocationData("Enemy Intelligence", "Enemy Intelligence: Northeast Garrison", SC2NCO_LOC_ID_OFFSET + 303, LocationType.EXTRA, + lambda state: logic.enemy_intelligence_first_stage_requirement(state)), + LocationData("Enemy Intelligence", "Enemy Intelligence: Southeast Garrison", SC2NCO_LOC_ID_OFFSET + 304, LocationType.EXTRA, + lambda state: logic.enemy_intelligence_first_stage_requirement(state) + and logic.enemy_intelligence_cliff_garrison(state)), + LocationData("Enemy Intelligence", "Enemy Intelligence: South Garrison", SC2NCO_LOC_ID_OFFSET + 305, LocationType.EXTRA, + lambda state: logic.enemy_intelligence_first_stage_requirement(state)), + LocationData("Enemy Intelligence", "Enemy Intelligence: All Garrisons", SC2NCO_LOC_ID_OFFSET + 306, LocationType.VANILLA, + lambda state: logic.enemy_intelligence_first_stage_requirement(state) + and logic.enemy_intelligence_cliff_garrison(state)), + LocationData("Enemy Intelligence", "Enemy Intelligence: Forces Rescued", SC2NCO_LOC_ID_OFFSET + 307, LocationType.VANILLA, + lambda state: logic.enemy_intelligence_first_stage_requirement(state)), + LocationData("Enemy Intelligence", "Enemy Intelligence: Communications Hub", SC2NCO_LOC_ID_OFFSET + 308, LocationType.VANILLA, + lambda state: logic.enemy_intelligence_second_stage_requirement(state)), + LocationData("Trouble In Paradise", "Trouble In Paradise: Victory", SC2NCO_LOC_ID_OFFSET + 400, LocationType.VICTORY, + lambda state: logic.trouble_in_paradise_requirement(state)), + LocationData("Trouble In Paradise", "Trouble In Paradise: North Base: West Hatchery", SC2NCO_LOC_ID_OFFSET + 401, LocationType.VANILLA, + lambda state: logic.trouble_in_paradise_requirement(state)), + LocationData("Trouble In Paradise", "Trouble In Paradise: North Base: North Hatchery", SC2NCO_LOC_ID_OFFSET + 402, LocationType.VANILLA, + lambda state: logic.trouble_in_paradise_requirement(state)), + LocationData("Trouble In Paradise", "Trouble In Paradise: North Base: East Hatchery", SC2NCO_LOC_ID_OFFSET + 403, LocationType.VANILLA), + LocationData("Trouble In Paradise", "Trouble In Paradise: South Base: Northwest Hatchery", SC2NCO_LOC_ID_OFFSET + 404, LocationType.VANILLA, + lambda state: logic.trouble_in_paradise_requirement(state)), + LocationData("Trouble In Paradise", "Trouble In Paradise: South Base: Southwest Hatchery", SC2NCO_LOC_ID_OFFSET + 405, LocationType.VANILLA, + lambda state: logic.trouble_in_paradise_requirement(state)), + LocationData("Trouble In Paradise", "Trouble In Paradise: South Base: East Hatchery", SC2NCO_LOC_ID_OFFSET + 406, LocationType.VANILLA), + LocationData("Trouble In Paradise", "Trouble In Paradise: North Shield Projector", SC2NCO_LOC_ID_OFFSET + 407, LocationType.EXTRA, + lambda state: logic.trouble_in_paradise_requirement(state)), + LocationData("Trouble In Paradise", "Trouble In Paradise: East Shield Projector", SC2NCO_LOC_ID_OFFSET + 408, LocationType.EXTRA, + lambda state: logic.trouble_in_paradise_requirement(state)), + LocationData("Trouble In Paradise", "Trouble In Paradise: South Shield Projector", SC2NCO_LOC_ID_OFFSET + 409, LocationType.EXTRA, + lambda state: logic.trouble_in_paradise_requirement(state)), + LocationData("Trouble In Paradise", "Trouble In Paradise: West Shield Projector", SC2NCO_LOC_ID_OFFSET + 410, LocationType.EXTRA, + lambda state: logic.trouble_in_paradise_requirement(state)), + LocationData("Trouble In Paradise", "Trouble In Paradise: Fleet Beacon", SC2NCO_LOC_ID_OFFSET + 411, LocationType.VANILLA, + lambda state: logic.trouble_in_paradise_requirement(state)), + LocationData("Night Terrors", "Night Terrors: Victory", SC2NCO_LOC_ID_OFFSET + 500, LocationType.VICTORY, + lambda state: logic.night_terrors_requirement(state)), + LocationData("Night Terrors", "Night Terrors: 1 Terrazine Node Collected", SC2NCO_LOC_ID_OFFSET + 501, LocationType.EXTRA, + lambda state: logic.night_terrors_requirement(state)), + LocationData("Night Terrors", "Night Terrors: 2 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 502, LocationType.EXTRA, + lambda state: logic.night_terrors_requirement(state)), + LocationData("Night Terrors", "Night Terrors: 3 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 503, LocationType.EXTRA, + lambda state: logic.night_terrors_requirement(state)), + LocationData("Night Terrors", "Night Terrors: 4 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 504, LocationType.EXTRA, + lambda state: logic.night_terrors_requirement(state)), + LocationData("Night Terrors", "Night Terrors: 5 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 505, LocationType.EXTRA, + lambda state: logic.night_terrors_requirement(state)), + LocationData("Night Terrors", "Night Terrors: HERC Outpost", SC2NCO_LOC_ID_OFFSET + 506, LocationType.VANILLA, + lambda state: logic.night_terrors_requirement(state)), + LocationData("Night Terrors", "Night Terrors: Umojan Mine", SC2NCO_LOC_ID_OFFSET + 507, LocationType.EXTRA, + lambda state: logic.night_terrors_requirement(state)), + LocationData("Night Terrors", "Night Terrors: Blightbringer", SC2NCO_LOC_ID_OFFSET + 508, LocationType.VANILLA, + lambda state: logic.night_terrors_requirement(state) + and logic.nova_ranged_weapon(state) + and state.has_any( + {ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PULSE_GRENADES, ItemNames.NOVA_STIM_INFUSION, + ItemNames.NOVA_HOLO_DECOY}, player)), + LocationData("Night Terrors", "Night Terrors: Science Facility", SC2NCO_LOC_ID_OFFSET + 509, LocationType.EXTRA, + lambda state: logic.night_terrors_requirement(state)), + LocationData("Night Terrors", "Night Terrors: Eradicators", SC2NCO_LOC_ID_OFFSET + 510, LocationType.VANILLA, + lambda state: logic.night_terrors_requirement(state) + and logic.nova_any_weapon(state)), + LocationData("Flashpoint", "Flashpoint: Victory", SC2NCO_LOC_ID_OFFSET + 600, LocationType.VICTORY, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Close North Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 601, LocationType.EXTRA, + lambda state: state.has_any( + {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player) + or logic.terran_common_unit(state)), + LocationData("Flashpoint", "Flashpoint: Close East Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 602, LocationType.EXTRA, + lambda state: state.has_any( + {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player) + or logic.terran_common_unit(state)), + LocationData("Flashpoint", "Flashpoint: Far North Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 603, LocationType.EXTRA, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Far East Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 604, LocationType.EXTRA, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Experimental Weapon", SC2NCO_LOC_ID_OFFSET + 605, LocationType.VANILLA, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Northwest Subway Entrance", SC2NCO_LOC_ID_OFFSET + 606, LocationType.VANILLA, + lambda state: state.has_any( + {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player) + and logic.terran_common_unit(state) + or logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Southeast Subway Entrance", SC2NCO_LOC_ID_OFFSET + 607, LocationType.VANILLA, + lambda state: state.has_any( + {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player) + and logic.terran_common_unit(state) + or logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Northeast Subway Entrance", SC2NCO_LOC_ID_OFFSET + 608, LocationType.VANILLA, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Expansion Hatchery", SC2NCO_LOC_ID_OFFSET + 609, LocationType.EXTRA, + lambda state: state.has(ItemNames.LIBERATOR_RAID_ARTILLERY, player) and logic.terran_common_unit(state) + or logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Baneling Spawns", SC2NCO_LOC_ID_OFFSET + 610, LocationType.EXTRA, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Mutalisk Spawns", SC2NCO_LOC_ID_OFFSET + 611, LocationType.EXTRA, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Nydus Worm Spawns", SC2NCO_LOC_ID_OFFSET + 612, LocationType.EXTRA, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Lurker Spawns", SC2NCO_LOC_ID_OFFSET + 613, LocationType.EXTRA, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Brood Lord Spawns", SC2NCO_LOC_ID_OFFSET + 614, LocationType.EXTRA, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("Flashpoint", "Flashpoint: Ultralisk Spawns", SC2NCO_LOC_ID_OFFSET + 615, LocationType.EXTRA, + lambda state: logic.flashpoint_far_requirement(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Victory", SC2NCO_LOC_ID_OFFSET + 700, LocationType.VICTORY, + lambda state: logic.enemy_shadow_victory(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Sewers: Domination Visor", SC2NCO_LOC_ID_OFFSET + 701, LocationType.VANILLA, + lambda state: logic.enemy_shadow_domination(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Sewers: Resupply Crate", SC2NCO_LOC_ID_OFFSET + 702, LocationType.EXTRA, + lambda state: logic.enemy_shadow_first_stage(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Sewers: Facility Access", SC2NCO_LOC_ID_OFFSET + 703, LocationType.VANILLA, + lambda state: logic.enemy_shadow_first_stage(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Northwest Door Lock", SC2NCO_LOC_ID_OFFSET + 704, LocationType.VANILLA, + lambda state: logic.enemy_shadow_door_controls(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Southeast Door Lock", SC2NCO_LOC_ID_OFFSET + 705, LocationType.VANILLA, + lambda state: logic.enemy_shadow_door_controls(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Plasma Rifle", SC2NCO_LOC_ID_OFFSET + 706, LocationType.VANILLA, + lambda state: logic.enemy_shadow_second_stage(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Blink Suit", SC2NCO_LOC_ID_OFFSET + 707, LocationType.VANILLA, + lambda state: logic.enemy_shadow_second_stage(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Advanced Weaponry", SC2NCO_LOC_ID_OFFSET + 708, LocationType.VANILLA, + lambda state: logic.enemy_shadow_second_stage(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Entrance Resupply Crate", SC2NCO_LOC_ID_OFFSET + 709, LocationType.EXTRA, + lambda state: logic.enemy_shadow_first_stage(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: West Resupply Crate", SC2NCO_LOC_ID_OFFSET + 710, LocationType.EXTRA, + lambda state: logic.enemy_shadow_second_stage(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: North Resupply Crate", SC2NCO_LOC_ID_OFFSET + 711, LocationType.EXTRA, + lambda state: logic.enemy_shadow_second_stage(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: East Resupply Crate", SC2NCO_LOC_ID_OFFSET + 712, LocationType.EXTRA, + lambda state: logic.enemy_shadow_second_stage(state)), + LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: South Resupply Crate", SC2NCO_LOC_ID_OFFSET + 713, LocationType.EXTRA, + lambda state: logic.enemy_shadow_second_stage(state)), + LocationData("Dark Skies", "Dark Skies: Victory", SC2NCO_LOC_ID_OFFSET + 800, LocationType.VICTORY, + lambda state: logic.dark_skies_requirement(state)), + LocationData("Dark Skies", "Dark Skies: First Squadron of Dominion Fleet", SC2NCO_LOC_ID_OFFSET + 801, LocationType.EXTRA, + lambda state: logic.dark_skies_requirement(state)), + LocationData("Dark Skies", "Dark Skies: Remainder of Dominion Fleet", SC2NCO_LOC_ID_OFFSET + 802, LocationType.EXTRA, + lambda state: logic.dark_skies_requirement(state)), + LocationData("Dark Skies", "Dark Skies: Ji'nara", SC2NCO_LOC_ID_OFFSET + 803, LocationType.EXTRA, + lambda state: logic.dark_skies_requirement(state)), + LocationData("Dark Skies", "Dark Skies: Science Facility", SC2NCO_LOC_ID_OFFSET + 804, LocationType.VANILLA, + lambda state: logic.dark_skies_requirement(state)), + LocationData("End Game", "End Game: Victory", SC2NCO_LOC_ID_OFFSET + 900, LocationType.VICTORY, + lambda state: logic.end_game_requirement(state) and logic.nova_any_weapon(state)), + LocationData("End Game", "End Game: Xanthos", SC2NCO_LOC_ID_OFFSET + 901, LocationType.VANILLA, + lambda state: logic.end_game_requirement(state)), ] beat_events = [] diff --git a/worlds/sc2/MissionTables.py b/worlds/sc2/MissionTables.py index 17b82d8aaa36..7de6c50ea292 100644 --- a/worlds/sc2/MissionTables.py +++ b/worlds/sc2/MissionTables.py @@ -50,6 +50,7 @@ def __init__(self, campaign_id: int, name: str, goal_priority: SC2CampaignGoalPr PROLOGUE = 4, "Whispers of Oblivion (Legacy of the Void: Prologue)", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS LOTV = 5, "Legacy of the Void", SC2CampaignGoalPriority.VERY_HARD, SC2Race.PROTOSS EPILOGUE = 6, "Into the Void (Legacy of the Void: Epilogue)", SC2CampaignGoalPriority.EPILOGUE, SC2Race.ANY + NCO = 7, "Nova Covert Ops", SC2CampaignGoalPriority.HARD, SC2Race.TERRAN class SC2Mission(Enum): @@ -124,10 +125,12 @@ def __init__(self, mission_id: int, name: str, campaign: SC2Campaign, area: str, PLANETFALL = 47, "Planetfall", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_planetfall" DEATH_FROM_ABOVE = 48, "Death From Above", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_death_from_above" THE_RECKONING = 49, "The Reckoning", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_the_reckoning" + # Prologue DARK_WHISPERS = 50, "Dark Whispers", SC2Campaign.PROLOGUE, "_1", SC2Race.PROTOSS, MissionPools.EASY, "ap_dark_whispers" GHOSTS_IN_THE_FOG = 51, "Ghosts in the Fog", SC2Campaign.PROLOGUE, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_ghosts_in_the_fog" EVIL_AWOKEN = 52, "Evil Awoken", SC2Campaign.PROLOGUE, "_3", SC2Race.PROTOSS, MissionPools.STARTER, "ap_evil_awoken", False + # LotV FOR_AIUR = 53, "For Aiur!", SC2Campaign.LOTV, "Aiur", SC2Race.ANY, MissionPools.STARTER, "ap_for_aiur", False THE_GROWING_SHADOW = 54, "The Growing Shadow", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_growing_shadow" @@ -148,11 +151,23 @@ def __init__(self, mission_id: int, name: str, campaign: SC2Campaign, area: str, TEMPLAR_S_RETURN = 69, "Templar's Return", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_templar_s_return", False THE_HOST = 70, "The Host", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.HARD, "ap_the_host", SALVATION = 71, "Salvation", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_salvation" + # Epilogue INTO_THE_VOID = 72, "Into the Void", SC2Campaign.EPILOGUE, "_1", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_into_the_void" THE_ESSENCE_OF_ETERNITY = 73, "The Essence of Eternity", SC2Campaign.EPILOGUE, "_2", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_the_essence_of_eternity" AMON_S_FALL = 74, "Amon's Fall", SC2Campaign.EPILOGUE, "_3", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_amon_s_fall" + # Nova Covert Ops + THE_ESCAPE = 75, "The Escape", SC2Campaign.NCO, "_1", SC2Race.ANY, MissionPools.MEDIUM, "ap_the_escape", False + SUDDEN_STRIKE = 76, "Sudden Strike", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.EASY, "ap_sudden_strike" + ENEMY_INTELLIGENCE = 77, "Enemy Intelligence", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_enemy_intelligence" + TROUBLE_IN_PARADISE = 78, "Trouble In Paradise", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_trouble_in_paradise" + NIGHT_TERRORS = 79, "Night Terrors", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_night_terrors" + FLASHPOINT = 80, "Flashpoint", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_flashpoint" + IN_THE_ENEMY_S_SHADOW = 81, "In the Enemy's Shadow", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_in_the_enemy_s_shadow", False + DARK_SKIES = 82, "Dark Skies", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.HARD, "ap_dark_skies" + END_GAME = 83, "End Game", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.HARD, "ap_end_game" + class MissionConnection: campaign: SC2Campaign @@ -168,6 +183,7 @@ def _asdict(self): "connect_to": self.connect_to } + class MissionInfo(NamedTuple): mission: SC2Mission required_world: List[Union[MissionConnection, Dict[Literal["campaign", "connect_to"], int]]] @@ -275,6 +291,17 @@ class FillMission(NamedTuple): FillMission(MissionPools.VERY_HARD, [MissionConnection(24, SC2Campaign.WOL), MissionConnection(19, SC2Campaign.HOTS), MissionConnection(18, SC2Campaign.LOTV)], "_1", completion_critical=True), FillMission(MissionPools.VERY_HARD, [MissionConnection(0, SC2Campaign.EPILOGUE)], "_2", completion_critical=True, removal_priority=1), FillMission(MissionPools.FINAL, [MissionConnection(1, SC2Campaign.EPILOGUE)], "_3", completion_critical=True), + ], + SC2Campaign.NCO: [ + FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.NCO)], "_1", completion_critical=True), + FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.NCO)], "_1", completion_critical=True), + FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.NCO)], "_1", completion_critical=True), + FillMission(MissionPools.HARD, [MissionConnection(2, SC2Campaign.NCO)], "_2", completion_critical=True), + FillMission(MissionPools.HARD, [MissionConnection(3, SC2Campaign.NCO)], "_2", completion_critical=True), + FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.NCO)], "_2", completion_critical=True), + FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.NCO)], "_3", completion_critical=True), + FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.NCO)], "_3", completion_critical=True), + FillMission(MissionPools.FINAL, [MissionConnection(7, SC2Campaign.NCO)], "_3", completion_critical=True), ] } mini_campaign_order: Dict[SC2Campaign, List[FillMission]] = { @@ -331,6 +358,13 @@ class FillMission(NamedTuple): SC2Campaign.EPILOGUE: [ FillMission(MissionPools.VERY_HARD, [MissionConnection(12, SC2Campaign.WOL), MissionConnection(12, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.LOTV)], "_1", completion_critical=True), FillMission(MissionPools.FINAL, [MissionConnection(0, SC2Campaign.EPILOGUE)], "_2", completion_critical=True), + ], + SC2Campaign.NCO: [ + FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.NCO)], "_1", completion_critical=True), + FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.NCO)], "_1", completion_critical=True), + FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.NCO)], "_2", completion_critical=True), + FillMission(MissionPools.HARD, [MissionConnection(2, SC2Campaign.NCO)], "_3", completion_critical=True), + FillMission(MissionPools.FINAL, [MissionConnection(3, SC2Campaign.NCO)], "_3", completion_critical=True), ] } @@ -516,6 +550,17 @@ class FillMission(NamedTuple): SC2Mission.INTO_THE_VOID.mission_name: MissionInfo(SC2Mission.INTO_THE_VOID, [MissionConnection(25, SC2Campaign.WOL), MissionConnection(20, SC2Campaign.HOTS), MissionConnection(19, SC2Campaign.LOTV)], SC2Mission.INTO_THE_VOID.area, completion_critical=True), SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name: MissionInfo(SC2Mission.THE_ESSENCE_OF_ETERNITY, [MissionConnection(1, SC2Campaign.EPILOGUE)], SC2Mission.THE_ESSENCE_OF_ETERNITY.area, completion_critical=True), SC2Mission.AMON_S_FALL.mission_name: MissionInfo(SC2Mission.AMON_S_FALL, [MissionConnection(2, SC2Campaign.EPILOGUE)], SC2Mission.AMON_S_FALL.area, completion_critical=True), + }, + SC2Campaign.NCO: { + SC2Mission.THE_ESCAPE.mission_name: MissionInfo(SC2Mission.THE_ESCAPE, [], SC2Mission.THE_ESCAPE.area, completion_critical=True), + SC2Mission.SUDDEN_STRIKE.mission_name: MissionInfo(SC2Mission.SUDDEN_STRIKE, [MissionConnection(1, SC2Campaign.NCO)], SC2Mission.SUDDEN_STRIKE.area, completion_critical=True), + SC2Mission.ENEMY_INTELLIGENCE.mission_name: MissionInfo(SC2Mission.ENEMY_INTELLIGENCE, [MissionConnection(2, SC2Campaign.NCO)], SC2Mission.ENEMY_INTELLIGENCE.area, completion_critical=True), + SC2Mission.TROUBLE_IN_PARADISE.mission_name: MissionInfo(SC2Mission.TROUBLE_IN_PARADISE, [MissionConnection(3, SC2Campaign.NCO)], SC2Mission.TROUBLE_IN_PARADISE.area, completion_critical=True), + SC2Mission.NIGHT_TERRORS.mission_name: MissionInfo(SC2Mission.NIGHT_TERRORS, [MissionConnection(4, SC2Campaign.NCO)], SC2Mission.NIGHT_TERRORS.area, completion_critical=True), + SC2Mission.FLASHPOINT.mission_name: MissionInfo(SC2Mission.FLASHPOINT, [MissionConnection(5, SC2Campaign.NCO)], SC2Mission.FLASHPOINT.area, completion_critical=True), + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name: MissionInfo(SC2Mission.IN_THE_ENEMY_S_SHADOW, [MissionConnection(6, SC2Campaign.NCO)], SC2Mission.IN_THE_ENEMY_S_SHADOW.area, completion_critical=True), + SC2Mission.DARK_SKIES.mission_name: MissionInfo(SC2Mission.DARK_SKIES, [MissionConnection(7, SC2Campaign.NCO)], SC2Mission.DARK_SKIES.area, completion_critical=True), + SC2Mission.END_GAME.mission_name: MissionInfo(SC2Mission.END_GAME, [MissionConnection(8, SC2Campaign.NCO)], SC2Mission.END_GAME.area, completion_critical=True), } } @@ -592,6 +637,7 @@ class SC2CampaignGoal(NamedTuple): SC2Campaign.PROLOGUE: SC2CampaignGoal(SC2Mission.EVIL_AWOKEN, "Evil Awoken: Victory"), SC2Campaign.LOTV: SC2CampaignGoal(SC2Mission.SALVATION, "Salvation: Victory"), SC2Campaign.EPILOGUE: None, + SC2Campaign.NCO: None, } campaign_alt_final_mission_locations: Dict[SC2Campaign, Dict[SC2Mission, str]] = { @@ -622,6 +668,13 @@ class SC2CampaignGoal(NamedTuple): SC2Mission.INTO_THE_VOID: "Into the Void: Victory", SC2Mission.THE_ESSENCE_OF_ETERNITY: "The Essence of Eternity: Victory", SC2Mission.AMON_S_FALL: "Amon's Fall: Victory" + }, + SC2Campaign.NCO: { + SC2Mission.END_GAME: "End Game: Victory", + SC2Mission.FLASHPOINT: "Flashpoint: Victory", + SC2Mission.DARK_SKIES: "Dark Skies: Victory", + SC2Mission.NIGHT_TERRORS: "Night Terrors: Victory", + SC2Mission.TROUBLE_IN_PARADISE: "Trouble In Paradise: Victory" } } diff --git a/worlds/sc2/Options.py b/worlds/sc2/Options.py index c321e1be604c..fa6de48ad5f5 100644 --- a/worlds/sc2/Options.py +++ b/worlds/sc2/Options.py @@ -83,8 +83,8 @@ class MaximumCampaignSize(Range): """ display_name = "Maximum Campaign Size" range_start = 1 - range_end = 74 - default = 74 + range_end = 83 + default = 83 class GridTwoStartPositions(Toggle): @@ -180,6 +180,15 @@ class EnableEpilogueMissions(DefaultOnToggle): display_name = "Enable Epilogue missions" +class EnableNCOMissions(DefaultOnToggle): + """ + Enables missions from Nova Covert Ops campaign. + + Note: For best gameplay experience it's recommended to also enable Wings of Liberty campaign. + """ + display_name = "Enable Nova Covert Ops missions" + + class ShuffleCampaigns(DefaultOnToggle): """ Shuffles the missions between campaigns if enabled. @@ -278,7 +287,11 @@ class GenericUpgradeItems(Choice): class NovaCovertOpsItems(Toggle): - """If turned on, the equipment upgrades from Nova Covert Ops may be present in the world.""" + """ + If turned on, the equipment upgrades from Nova Covert Ops may be present in the world. + + If Nova Covert Ops campaign is enabled, this option is locked to be turned on. + """ display_name = "Nova Covert Ops Items" default = Toggle.option_true @@ -672,6 +685,7 @@ class StartingSupplyPerItem(Range): "enable_lotv_prologue_missions": EnableLotVPrologueMissions, "enable_lotv_missions": EnableLotVMissions, "enable_epilogue_missions": EnableEpilogueMissions, + "enable_nco_missions": EnableNCOMissions, "shuffle_campaigns": ShuffleCampaigns, "shuffle_no_build": ShuffleNoBuild, "starter_unit": StarterUnit, @@ -734,6 +748,8 @@ def get_enabled_campaigns(multiworld: MultiWorld, player: int) -> Set[SC2Campaig enabled_campaigns.add(SC2Campaign.LOTV) if get_option_value(multiworld, player, "enable_epilogue_missions"): enabled_campaigns.add(SC2Campaign.EPILOGUE) + if get_option_value(multiworld, player, "enable_nco_missions"): + enabled_campaigns.add(SC2Campaign.NCO) return enabled_campaigns diff --git a/worlds/sc2/PoolFilter.py b/worlds/sc2/PoolFilter.py index c0def13df51e..7c92b07d9ed6 100644 --- a/worlds/sc2/PoolFilter.py +++ b/worlds/sc2/PoolFilter.py @@ -141,6 +141,7 @@ def move_mission(mission: SC2Mission, current_pool, new_pool): # Additional starter mission if player is granted story tech move_mission(SC2Mission.ENEMY_WITHIN, MissionPools.EASY, MissionPools.STARTER) move_mission(SC2Mission.TEMPLAR_S_RETURN, MissionPools.EASY, MissionPools.STARTER) + move_mission(SC2Mission.THE_ESCAPE, MissionPools.MEDIUM, MissionPools.STARTER) if grant_story_tech or kerriganless: # The player has, all the stuff he needs, provided under these settings move_mission(SC2Mission.SUPREME, MissionPools.MEDIUM, MissionPools.STARTER) @@ -153,6 +154,9 @@ def move_mission(mission: SC2Mission, current_pool, new_pool): move_mission(SC2Mission.DOMINATION, MissionPools.EASY, MissionPools.STARTER) if len(mission_pools[MissionPools.STARTER]) < 2: move_mission(SC2Mission.TEMPLAR_S_RETURN, MissionPools.EASY, MissionPools.STARTER) + if len(mission_pools[MissionPools.STARTER]) + len(mission_pools[MissionPools.EASY]) < 2: + # Flashpoint needs just a few items at start but competent comp at the end + move_mission(SC2Mission.FLASHPOINT, MissionPools.HARD, MissionPools.EASY) remove_final_mission_from_other_pools(mission_pools) return mission_pools @@ -178,6 +182,7 @@ def get_item_upgrades(inventory: List[Item], parent_item: Union[Item, str]) -> L def get_item_quantity(item: Item, multiworld: MultiWorld, player: int): if (not get_option_value(multiworld, player, "nco_items")) \ + and SC2Campaign.NCO in get_disabled_campaigns(multiworld, player) \ and item.name in progressive_if_nco: return 1 if (not get_option_value(multiworld, player, "ext_items")) \ diff --git a/worlds/sc2/Regions.py b/worlds/sc2/Regions.py index c77e76d3e894..0b8af4bf3086 100644 --- a/worlds/sc2/Regions.py +++ b/worlds/sc2/Regions.py @@ -247,6 +247,25 @@ def wol_cleared_missions(state: CollectionState, mission_count: int) -> bool: connect(multiworld, player, names, "The Essence of Eternity", "Amon's Fall", lambda state: state.has("Beat The Essence of Eternity", player)), + if SC2Campaign.NCO in enabled_campaigns: + connect(multiworld, player, names, "Menu", "The Escape") + connect(multiworld, player, names, "The Escape", "Sudden Strike", + lambda state: state.has("Beat The Escape", player)) + connect(multiworld, player, names, "Sudden Strike", "Enemy Intelligence", + lambda state: state.has("Beat Sudden Strike", player)) + connect(multiworld, player, names, "Enemy Intelligence", "Trouble In Paradise", + lambda state: state.has("Beat Enemy Intelligence", player)) + connect(multiworld, player, names, "Trouble In Paradise", "Night Terrors", + lambda state: state.has("Beat Evacuation", player)) + connect(multiworld, player, names, "Night Terrors", "Flashpoint", + lambda state: state.has("Beat Night Terrors", player)) + connect(multiworld, player, names, "Flashpoint", "In the Enemy's Shadow", + lambda state: state.has("Beat Flashpoint", player)) + connect(multiworld, player, names, "In the Enemy's Shadow", "Dark Skies", + lambda state: state.has("Beat In the Enemy's Shadow", player)) + connect(multiworld, player, names, "Dark Skies", "End Game", + lambda state: state.has("Beat Dark Skies", player)) + goal_location = get_goal_location(final_mission) assert goal_location, f"Unable to find a goal location for mission {final_mission}" setup_final_location(goal_location, location_cache) diff --git a/worlds/sc2/Rules.py b/worlds/sc2/Rules.py index 233decc7bf67..15547626ea79 100644 --- a/worlds/sc2/Rules.py +++ b/worlds/sc2/Rules.py @@ -1,9 +1,9 @@ from BaseClasses import MultiWorld, CollectionState from worlds.AutoWorld import LogicMixin from .Options import get_option_value, RequiredTactics, kerrigan_unit_available, AllInMap, GameDifficulty, \ - GrantStoryTech, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence + GrantStoryTech, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence, get_enabled_campaigns, MissionOrder from .Items import get_basic_units, defense_ratings, zerg_defense_ratings, kerrigan_actives, air_defense_ratings -from .MissionTables import SC2Race +from .MissionTables import SC2Race, SC2Campaign from . import ItemNames @@ -135,6 +135,9 @@ def terran_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_en defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if state.has(item, self.player))) if air_enemy: defense_score += sum((air_defense_ratings[item] for item in air_defense_ratings if state.has(item, self.player))) + if air_enemy and zerg_enemy and state.has(ItemNames.VALKYRIE, self.player): + # Valkyries shred mass Mutas, most common air enemy that's massed in these cases + defense_score += 2 # Advanced Tactics bumps defense rating requirements down by 2 if self.advanced_tactics: defense_score += 2 @@ -639,6 +642,227 @@ def amons_fall_requirement(self, state: CollectionState) -> bool: else: return state.has(ItemNames.MUTALISK, self.player) and self.zerg_competent_comp(state) + def nova_any_weapon(self, state: CollectionState) -> bool: + return state.has_any( + {ItemNames.NOVA_C20A_CANISTER_RIFLE, ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PLASMA_RIFLE, + ItemNames.NOVA_MONOMOLECULAR_BLADE, ItemNames.NOVA_BLAZEFIRE_GUNBLADE}, self.player) + + def nova_ranged_weapon(self, state: CollectionState) -> bool: + return state.has_any( + {ItemNames.NOVA_C20A_CANISTER_RIFLE, ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PLASMA_RIFLE}, + self.player) + + def nova_splash(self, state: CollectionState) -> bool: + return state.has_any({ + ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_BLAZEFIRE_GUNBLADE, ItemNames.NOVA_PULSE_GRENADES + }, self.player) \ + or self.advanced_tactics and state.has_any( + {ItemNames.NOVA_PLASMA_RIFLE, ItemNames.NOVA_MONOMOLECULAR_BLADE}, self.player) + + def nova_dash(self, state: CollectionState) -> bool: + return state.has_any({ItemNames.NOVA_MONOMOLECULAR_BLADE, ItemNames.NOVA_BLINK}, self.player) + + def nova_full_stealth(self, state: CollectionState) -> bool: + return state.count(ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player) >= 2 + + def nova_heal(self, state: CollectionState) -> bool: + return state.has_any({ItemNames.NOVA_ARMORED_SUIT_MODULE, ItemNames.NOVA_STIM_INFUSION}, self.player) + + def nova_escape_assist(self, state: CollectionState) -> bool: + return state.has_any({ItemNames.NOVA_BLINK, ItemNames.NOVA_HOLO_DECOY, ItemNames.NOVA_IONIC_FORCE_FIELD}, self.player) + + def the_escape_stuff_granted(self) -> bool: + """ + The NCO first mission requires having too much stuff first before actually able to do anything + :return: + """ + return self.story_tech_granted \ + or (self.mission_order == MissionOrder.option_vanilla and self.enabled_campaigns == {SC2Campaign.NCO}) + + def the_escape_first_stage_requirement(self, state: CollectionState) -> bool: + return self.the_escape_stuff_granted() \ + or (self.nova_ranged_weapon(state) + and self.nova_full_stealth(state) or self.nova_heal(state)) + + def the_escape_requirement(self, state: CollectionState) -> bool: + return self.the_escape_first_stage_requirement(state) \ + and (self.the_escape_stuff_granted() or self.nova_splash(state)) + + def terran_cliffjumper(self, state: CollectionState) -> bool: + return state.has(ItemNames.REAPER, self.player) \ + or state.has_all({ItemNames.GOLIATH, ItemNames.GOLIATH_JUMP_JETS}, self.player) \ + or state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_JUMP_JETS}, self.player) + + def terran_able_to_snipe_defiler(self, state: CollectionState) -> bool: + return state.has_all({ItemNames.NOVA_JUMP_SUIT_MODULE, ItemNames.NOVA_C20A_CANISTER_RIFLE}, self.player) \ + or state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_MAELSTROM_ROUNDS, ItemNames.SIEGE_TANK_JUMP_JETS}, self.player) + + def sudden_strike_requirement(self, state: CollectionState) -> bool: + return self.sudden_strike_can_reach_objectives(state) \ + and self.terran_able_to_snipe_defiler(state) \ + and state.has_any({ItemNames.SIEGE_TANK, ItemNames.VULTURE}, self.player) \ + and self.terran_defense_rating(state, True, False) > 5 + + def sudden_strike_can_reach_objectives(self, state: CollectionState) -> bool: + return self.terran_cliffjumper(state) \ + or state.has_any({ItemNames.BANSHEE, ItemNames.VIKING}, self.player) \ + or ( + self.advanced_tactics + and state.has(ItemNames.MEDIVAC, self.player) + and state.has_any({ItemNames.MARINE, ItemNames.MARAUDER, ItemNames.VULTURE, ItemNames.HELLION, + ItemNames.GOLIATH}, self.player) + ) + + def enemy_intelligence_garrisonable_unit(self, state: CollectionState) -> bool: + """ + Has unit usable as a Garrison in Enemy Intelligence + :param state: + :return: + """ + return state.has_any( + {ItemNames.MARINE, ItemNames.REAPER, ItemNames.MARAUDER, ItemNames.GHOST, ItemNames.SPECTRE, + ItemNames.HELLION, ItemNames.GOLIATH, ItemNames.WARHOUND, ItemNames.DIAMONDBACK, ItemNames.VIKING}, + self.player) + + def enemy_intelligence_cliff_garrison(self, state: CollectionState) -> bool: + return state.has_any({ItemNames.REAPER, ItemNames.VIKING, ItemNames.MEDIVAC, ItemNames.HERCULES}, self.player) \ + or state.has_all({ItemNames.GOLIATH, ItemNames.GOLIATH_JUMP_JETS}, self.player) \ + or self.advanced_tactics and state.has_any({ItemNames.HELS_ANGELS, ItemNames.BRYNHILDS}, self.player) + + def enemy_intelligence_first_stage_requirement(self, state: CollectionState) -> bool: + return self.enemy_intelligence_garrisonable_unit(state) \ + and (self.terran_competent_comp(state) + or ( + self.terran_common_unit(state) + and self.terran_competent_anti_air(state) + and state.has(ItemNames.NOVA_NUKE, self.player) + ) + ) \ + and self.terran_defense_rating(state, True, True) >= 5 + + def enemy_intelligence_second_stage_requirement(self, state: CollectionState) -> bool: + return self.enemy_intelligence_first_stage_requirement(state) \ + and self.enemy_intelligence_cliff_garrison(state) \ + and ( + self.story_tech_granted + or ( + self.nova_any_weapon(state) + and ( + self.nova_full_stealth(state) + or (self.nova_heal(state) + and self.nova_splash(state) + and self.nova_ranged_weapon(state)) + ) + ) + ) + + def enemy_intelligence_third_stage_requirement(self, state: CollectionState) -> bool: + return self.enemy_intelligence_second_stage_requirement(state) \ + and ( + self.story_tech_granted + or ( + state.has(ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player) + and self.nova_dash(state) + ) + ) + + def trouble_in_paradise_requirement(self, state: CollectionState) -> bool: + return self.nova_any_weapon(state) \ + and self.nova_splash(state) \ + and self.terran_beats_protoss_deathball(state) \ + and self.terran_defense_rating(state, True, True) > 6 + + def night_terrors_requirement(self, state: CollectionState) -> bool: + return self.terran_common_unit(state) \ + and self.terran_competent_anti_air(state) \ + and ( + # These can handle the waves of infested, even volatile ones + state.has(ItemNames.SIEGE_TANK, self.player) + or state.has_all({ItemNames.VIKING, ItemNames.VIKING_SHREDDER_ROUNDS}, self.player) + or ( + ( + # Regular infesteds + state.has(ItemNames.FIREBAT, self.player) + or state.has_all({ItemNames.HELLION, ItemNames.HELLION_HELLBAT_ASPECT}, self.player) + or ( + self.advanced_tactics + and state.has_any({ItemNames.PERDITION_TURRET, ItemNames.PLANETARY_FORTRESS}, self.player) + ) + ) + and self.terran_bio_heal(state) + and ( + # Volatile infesteds + state.has(ItemNames.LIBERATOR, self.player) + or ( + self.advanced_tactics + and state.has_any({ItemNames.HERC, ItemNames.VULTURE}, self.player) + ) + ) + ) + ) + + def flashpoint_far_requirement(self, state: CollectionState) -> bool: + return self.terran_competent_comp(state) \ + and self.terran_defense_rating(state, True, False) >= 6 + + def enemy_shadow_tripwires_tool(self, state: CollectionState) -> bool: + return state.has_any({ItemNames.NOVA_FLASHBANG_GRENADES, ItemNames.NOVA_BLINK, ItemNames.NOVA_DOMINATION}, + self.player) + + def enemy_shadow_door_unlocks_tool(self, state: CollectionState) -> bool: + return state.has_any({ItemNames.NOVA_DOMINATION, ItemNames.NOVA_BLINK, ItemNames.NOVA_JUMP_SUIT_MODULE}, + self.player) + + def enemy_shadow_domination(self, state: CollectionState) -> bool: + return self.story_tech_granted \ + or (self.nova_ranged_weapon(state) + and (self.nova_full_stealth(state) + or state.has(ItemNames.NOVA_JUMP_SUIT_MODULE, self.player) + or (self.nova_heal(state) and self.nova_splash(state)) + ) + ) + + def enemy_shadow_first_stage(self, state: CollectionState) -> bool: + return self.enemy_shadow_domination(state) \ + and (self.story_tech_granted + or ((self.nova_full_stealth(state) and self.enemy_shadow_tripwires_tool(state)) + or (self.nova_heal(state) and self.nova_splash(state)) + ) + ) + + def enemy_shadow_second_stage(self, state: CollectionState) -> bool: + return self.enemy_shadow_first_stage(state) \ + and (self.story_tech_granted + or self.nova_splash(state) + or self.nova_heal(state) + or self.nova_escape_assist(state) + ) + + def enemy_shadow_door_controls(self, state: CollectionState) -> bool: + return self.enemy_shadow_second_stage(state) \ + and (self.story_tech_granted or self.enemy_shadow_door_unlocks_tool(state)) + + def enemy_shadow_victory(self, state: CollectionState) -> bool: + return self.enemy_shadow_door_controls(state) \ + and (self.story_tech_granted or self.nova_heal(state)) + + def dark_skies_requirement(self, state: CollectionState) -> bool: + return self.terran_common_unit(state) \ + and self.terran_beats_protoss_deathball(state) \ + and self.terran_defense_rating(state, False, True) >= 8 + + def end_game_requirement(self, state: CollectionState) -> bool: + return self.terran_competent_comp(state) \ + and ( + state.has_any({ItemNames.BATTLECRUISER, ItemNames.LIBERATOR, ItemNames.BANSHEE}, self.player) + or state.has_all({ItemNames.WRAITH, ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player) + ) \ + and (state.has_any({ItemNames.BATTLECRUISER, ItemNames.VIKING, ItemNames.LIBERATOR}, self.player) + or (self.advanced_tactics + and state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, self.player) + ) + ) + def __init__(self, multiworld: MultiWorld, player: int): self.multiworld = multiworld self.player = player @@ -651,3 +875,5 @@ def __init__(self, multiworld: MultiWorld, player: int): self.basic_zerg_units = get_basic_units(self.multiworld, self.player, SC2Race.ZERG) self.basic_protoss_units = get_basic_units(self.multiworld, self.player, SC2Race.PROTOSS) self.spear_of_adun_autonomously_cast_presence = get_option_value(multiworld, player, "spear_of_adun_autonomously_cast_ability_presence") + self.enabled_campaigns = get_enabled_campaigns(multiworld, player) + self.mission_order = get_option_value(multiworld, player, "mission_order") diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index 659d5913fd25..99ab9512895e 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -8,7 +8,7 @@ from .Items import StarcraftItem, filler_items, item_name_groups, get_item_table, get_full_item_list, \ get_basic_units, ItemData, upgrade_included_names, progressive_if_nco, kerrigan_actives, kerrigan_passives, \ kerrigan_only_passives, progressive_if_ext, not_balanced_starting_units, spear_of_adun_calldowns, \ - spear_of_adun_castable_passives + spear_of_adun_castable_passives, nova_equimpent from .Locations import get_locations, LocationType, get_location_types, get_plando_locations from .Regions import create_regions from .Options import sc2_options, get_option_value, LocationInclusion, KerriganLevelItemDistribution, \ @@ -114,6 +114,7 @@ def fill_slot_data(self): 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["nova_covert_ops_only"] = (get_enabled_campaigns(self.multiworld, self.player) == {SC2Campaign.NCO}) slot_data["mission_req"] = slot_req_table slot_data["final_mission"] = self.final_mission_id slot_data["version"] = 3 @@ -168,6 +169,10 @@ def smart_exclude(item_choices: Set[str], choices_to_keep: int): if exclude_amount > 0: excluded_items.update(multiworld.random.sample(candidates, exclude_amount)) + # Nova gear exclusion if NCO not in campaigns + if SC2Campaign.NCO not in enabled_campaigns: + excluded_items.union(nova_equimpent) + kerrigan_presence = get_option_value(multiworld, player, "kerrigan_presence") # no Kerrigan & remove all passives => remove all abilities if kerrigan_presence == KerriganPresence.option_not_present_and_no_passives: @@ -226,11 +231,14 @@ def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Se basic_units = get_basic_units(multiworld, player, first_race) if starter_unit == StarterUnit.option_balanced: basic_units = basic_units.difference(not_balanced_starting_units) - if first_mission == SC2Mission.DARK_WHISPERS: + if first_mission == SC2Mission.DARK_WHISPERS.mission_name: # Special case - you don't have a logicless location but need an AA basic_units = basic_units.difference( {ItemNames.ZEALOT, ItemNames.CENTURION, ItemNames.SENTINEL, ItemNames.BLOOD_HUNTER, ItemNames.AVENGER, ItemNames.IMMORTAL, ItemNames.ANNIHILATOR, ItemNames.VANGUARD}) + if first_mission == SC2Mission.SUDDEN_STRIKE.mission_name: + # Special case - cliffjumpers + basic_units = {ItemNames.REAPER, ItemNames.GOLIATH, ItemNames.SIEGE_TANK} local_basic_unit = sorted(item for item in basic_units if item not in non_local_items and item not in excluded_items) if not local_basic_unit: # Drop non_local_items constraint @@ -239,6 +247,20 @@ def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Se raise Exception("Early Unit: At least one basic unit must be included") starter_items.append(add_starter_item(multiworld, player, excluded_items, local_basic_unit)) + + # NCO-only specific rules + if first_mission == SC2Mission.SUDDEN_STRIKE.mission_name: + support_item: str | None = None + if local_basic_unit == ItemNames.REAPER: + support_item = ItemNames.REAPER_SPIDER_MINES + elif local_basic_unit == ItemNames.GOLIATH: + support_item = ItemNames.GOLIATH_JUMP_JETS + elif local_basic_unit == ItemNames.SIEGE_TANK: + support_item = ItemNames.SIEGE_TANK_JUMP_JETS + if support_item is not None: + starter_items.append(add_starter_item(multiworld, player, excluded_items, support_item)) + if lookup_name_to_mission[first_mission].campaign == SC2Campaign.NCO: + starter_items.append(add_starter_item(multiworld, player, excluded_items, ItemNames.LIBERATOR_RAID_ARTILLERY)) starter_abilities = get_option_value(multiworld, player, 'start_primary_abilities') assert isinstance(starter_abilities, int) @@ -254,7 +276,7 @@ def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Se abilities = kerrigan_actives[tier].union(kerrigan_passives[tier]).difference(excluded_items) if abilities: ability_count -= 1 - starter_items.append(add_starter_item(multiworld, player, excluded_items, abilities)) + starter_items.append(add_starter_item(multiworld, player, excluded_items, list(abilities))) if ability_count == 0: break @@ -301,7 +323,8 @@ def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[S # Include items from outside main campaigns item_sets = {'wol', 'hots', 'lotv'} - if get_option_value(multiworld, player, 'nco_items'): + if get_option_value(multiworld, player, 'nco_items') \ + or SC2Campaign.NCO in get_enabled_campaigns(multiworld, player): item_sets.add('nco') if get_option_value(multiworld, player, 'bw_items'): item_sets.add('bw') @@ -394,7 +417,7 @@ def fill_resource_locations(multiworld: MultiWorld, player, locked_locations: Li item_name = multiworld.random.choice(filler_items) item = create_item_with_correct_settings(player, item_name) location.place_locked_item(item) - locked_locations.append(item) + locked_locations.append(location.name) def place_exclusion_item(item_name, location, locked_locations, player):