From e52fc0f5a2b9c3cdce0ee815aeef973a728d6190 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 28 Dec 2024 02:16:02 -0800 Subject: [PATCH 1/9] sc2: Fixing a typo in trireme item name and some minor fixes to descriptions --- worlds/sc2/item/item_descriptions.py | 6 +++--- worlds/sc2/item/item_names.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/sc2/item/item_descriptions.py b/worlds/sc2/item/item_descriptions.py index a62f414c7463..ea31e32abdd3 100644 --- a/worlds/sc2/item/item_descriptions.py +++ b/worlds/sc2/item/item_descriptions.py @@ -734,8 +734,8 @@ def _ability_desc(unit_name_plural: str, ability_name: str, ability_description: item_names.INFESTED_BUNKER_ENGORGED_BUNKERS: "Infested Bunkers gain +2 cargo slots. Infested Trooper spawn cooldown is reduced by 20%.", item_names.INFESTED_MISSILE_TURRET_BIOELECTRIC_PAYLOAD: "Increases anti-mechanical damage of Infested Missile Turrets by +6 per missile.", item_names.INFESTED_MISSILE_TURRET_ACID_SPORE_VENTS: "Infested Missile Turrets gain a secondary weapon that applies Devourer Acid Spores in an area around the target.", - item_names.TYRANNOZOR_TYRANTS_PROTECTION: "Tyrannozors grants nearby friendly units 2 armor.", - item_names.TYRANNOZOR_BARRAGE_OF_SPIKES: "Unleash a Barrage of Spikes, dealing 100 damage to enemy ground and air units around the Tyrannozor.", + item_names.TYRANNOZOR_TYRANTS_PROTECTION: "Tyrannozors grant nearby friendly units 2 armor.", + item_names.TYRANNOZOR_BARRAGE_OF_SPIKES: _ability_desc("Tyrannozors", "Barrage of Spikes", "deals 100 damage to enemy ground and air units around the Tyrannozor"), item_names.TYRANNOZOR_IMPALING_STRIKE: "Ultralisk and Tyrannozor melee attacks have a 20% chance to stun for 2 seconds.", item_names.TYRANNOZOR_HEALING_ADAPTATION: "Ultralisks and Tyrannozors regenerate life quickly when out of combat.", item_names.BILE_LAUNCHER_ARTILLERY_DUCTS: "Increases Bile Launcher range by +8.", @@ -990,7 +990,7 @@ def _ability_desc(unit_name_plural: str, ability_name: str, ability_description: item_names.ENERGIZER_RECLAMATION: _ability_desc("Energizers", "Reclamation", "temporarily takes control of an enemy mechanical unit. When the ability expires, the enemy unit self-destructs"), item_names.ENERGIZER_FORGED_CHASSIS: "Increases Energizer Life by +20.", item_names.HAVOC_DETECT_WEAKNESS: "Havocs' Target Lock gives an additional +15% damage bonus.", - item_names.HAVOC_BLOODSHARD_RESONANCE: "Havoc gain increased range for Squad Sight, Target Lock, and Force Field.", + item_names.HAVOC_BLOODSHARD_RESONANCE: "Havocs gain increased range for Squad Sight, Target Lock, and Force Field.", item_names.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS: "Zealots, Sentinels, and Centurions gain increased movement speed.", item_names.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY: "Zealots, Sentinels, and Centurions gain +30 maximum shields.", item_names.ZEALOT_WHIRLWIND: "Zealot War Council upgrade. Gives Zealots the whirlwind ability, dealing damage in an area over 3 seconds.", diff --git a/worlds/sc2/item/item_names.py b/worlds/sc2/item/item_names.py index 9d9d41c3e829..b739eea8697b 100644 --- a/worlds/sc2/item/item_names.py +++ b/worlds/sc2/item/item_names.py @@ -833,7 +833,7 @@ DAWNBRINGER_SOLARITE_LENS = "Solarite Lens (Dawnbringer)" CARRIER_REPAIR_DRONES = "Repair Drones (Carrier)" SKYLORD_HYPERJUMP = "Hyperjump (Skylord)" -TRIREME_SOLAR_BEAM = "Solar Beam (Trieme)" +TRIREME_SOLAR_BEAM = "Solar Beam (Trireme)" TEMPEST_DISINTEGRATION = "Disintegration (Tempest)" # Scout ARBITER_ABILITY_EFFICIENCY = "Ability Efficiency (Arbiter)" From cdb8cc1b73fb66b7786fd5434232055d6ed040e2 Mon Sep 17 00:00:00 2001 From: Magnemania Date: Sat, 28 Dec 2024 11:20:08 -0500 Subject: [PATCH 2/9] Implemented Mission Bias option Signed-off-by: Magnemania --- worlds/sc2/mission_order/mission_pools.py | 22 +++++++++++++++------- worlds/sc2/mission_order/structs.py | 9 +++++++-- worlds/sc2/options.py | 11 +++++++++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/worlds/sc2/mission_order/mission_pools.py b/worlds/sc2/mission_order/mission_pools.py index 16a528daaf1c..3e4d274e332e 100644 --- a/worlds/sc2/mission_order/mission_pools.py +++ b/worlds/sc2/mission_order/mission_pools.py @@ -182,7 +182,7 @@ def _add_mission_stats(self, mission: SC2Mission) -> None: self._used_flags[flag] += 1 self._used_missions.append(mission) - def pull_random_mission(self, world: World, slot: 'SC2MOGenMission', *, prefer_close_difficulty: bool = False) -> SC2Mission: + def pull_random_mission(self, world: World, slot: 'SC2MOGenMission', *, prefer_min_difficulty: Difficulty = None) -> SC2Mission: """Picks a random mission from the mission pool of the given slot and marks it as present in the mission order. With `prefer_close_difficulty = True` the mission is picked to be as close to the slot's desired difficulty as possible.""" @@ -195,7 +195,7 @@ def pull_random_mission(self, world: World, slot: 'SC2MOGenMission', *, prefer_c final_pool: List[int] = [] desired_difficulty = slot.option_difficulty - if prefer_close_difficulty: + if prefer_min_difficulty is None: # Iteratively look down and up around the slot's desired difficulty # Either a difficulty with valid missions is found, or an error is raised difficulty_offset = 0 @@ -214,14 +214,22 @@ def pull_random_mission(self, world: World, slot: 'SC2MOGenMission', *, prefer_c else: # Consider missions from all lower difficulties as well the desired difficulty + min_difficulty = prefer_min_difficulty + while len(final_pool) == 0: + final_pool = [ + mission + for difficulty in range(min_difficulty, desired_difficulty + 1) + for mission in difficulty_pools[difficulty] + ] + if min_difficulty <= Difficulty.STARTER: + break + min_difficulty -= 1 # Only take from higher difficulties if no lower difficulty is possible - final_pool = [ - mission - for difficulty in range(Difficulty.STARTER, desired_difficulty + 1) - for mission in difficulty_pools[difficulty] - ] difficulty_offset = 1 while len(final_pool) == 0: + # Ban difficulty offsets greater than 1 when reverse filling to avoid unnatural difficulty spikes + if difficulty_offset > 1 and prefer_min_difficulty == desired_difficulty: + raise Exception("Invalid mission pool for this mission order. Try changing your Mission Bias option to easy.") higher_difficulty = desired_difficulty + difficulty_offset if higher_difficulty > Difficulty.VERY_HARD: raise IndexError() diff --git a/worlds/sc2/mission_order/structs.py b/worlds/sc2/mission_order/structs.py index 90d375f0f75a..84498a7b7280 100644 --- a/worlds/sc2/mission_order/structs.py +++ b/worlds/sc2/mission_order/structs.py @@ -415,6 +415,7 @@ def fill_missions( locations_per_region = get_locations_per_region(locations) regions: List[Region] = [create_region(world, locations_per_region, location_cache, "Menu")] locked_ids = [lookup_name_to_mission[mission].id for mission in locked_missions] + prefer_easy_missions = world.options.mission_bias.value == world.options.mission_bias.option_easy # Resolve slots with set mission names for mission_slot in self.fixed_missions: @@ -435,6 +436,9 @@ def fill_missions( world.random.shuffle(self.sorted_missions[difficulty]) sorted_goals.extend(mission for mission in self.sorted_missions[difficulty] if mission in self.goal_missions) all_slots = [slot for diff in sorted(self.sorted_missions.keys()) for slot in self.sorted_missions[diff]] + # Sort standard slot difficulties from highest to lowest when using hard bias + if not prefer_easy_missions: + all_slots.reverse() all_slots.sort(key = lambda slot: len(slot.option_mission_pool.intersection(self.mission_pools.master_list))) sorted_goals.reverse() @@ -465,7 +469,7 @@ def fill_missions( # Pick goal missions first with stricter difficulty matching, and starting with harder goals for goal_slot in sorted_goals: try: - mission = self.mission_pools.pull_random_mission(world, goal_slot, prefer_close_difficulty=True) + mission = self.mission_pools.pull_random_mission(world, goal_slot) goal_slot.set_mission(world, mission, locations_per_region, location_cache) regions.append(goal_slot.region) all_slots.remove(goal_slot) @@ -479,7 +483,8 @@ def fill_missions( remaining_count = len(all_slots) for mission_slot in all_slots: try: - mission = self.mission_pools.pull_random_mission(world, mission_slot) + prefer_min_difficulty = Difficulty.STARTER if prefer_easy_missions else mission_slot.option_difficulty + mission = self.mission_pools.pull_random_mission(world, mission_slot, prefer_min_difficulty=prefer_min_difficulty) mission_slot.set_mission(world, mission, locations_per_region, location_cache) regions.append(mission_slot.region) remaining_count -= 1 diff --git a/worlds/sc2/options.py b/worlds/sc2/options.py index 1473428eda71..29f28c66a7c9 100644 --- a/worlds/sc2/options.py +++ b/worlds/sc2/options.py @@ -863,6 +863,16 @@ class ExcludedMissions(Sc2MissionSet): valid_keys = {mission.mission_name for mission in SC2Mission} +class MissionBias(Choice): + """ + When building a campaign, determines whether easy or hard missions are more likely to appear. + Only applies to mission orders with fewer missions than those available. + """ + display_name = "Mission Bias" + option_easy = 0 + option_hard = 1 + + class ExcludeVeryHardMissions(Choice): """ Excludes Very Hard missions outside of Epilogue campaign (All-In, The Reckoning, Salvation, and all Epilogue missions are considered Very Hard). @@ -1207,6 +1217,7 @@ class Starcraft2Options(PerGameCommonOptions): excluded_items: ExcludedItems unexcluded_items: UnexcludedItems excluded_missions: ExcludedMissions + mission_bias: MissionBias exclude_very_hard_missions: ExcludeVeryHardMissions vanilla_items_only: VanillaItemsOnly victory_cache: VictoryCache From bb2e03786a5c5ff647f50324ee6c80b57c0086dc Mon Sep 17 00:00:00 2001 From: Magnemania Date: Sat, 28 Dec 2024 11:54:45 -0500 Subject: [PATCH 3/9] Using existing flexible mission selection for random pull Signed-off-by: Magnemania --- worlds/sc2/mission_order/mission_pools.py | 28 ++++++++--------------- worlds/sc2/mission_order/structs.py | 5 ++-- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/worlds/sc2/mission_order/mission_pools.py b/worlds/sc2/mission_order/mission_pools.py index 3e4d274e332e..eb7c7599262a 100644 --- a/worlds/sc2/mission_order/mission_pools.py +++ b/worlds/sc2/mission_order/mission_pools.py @@ -182,12 +182,12 @@ def _add_mission_stats(self, mission: SC2Mission) -> None: self._used_flags[flag] += 1 self._used_missions.append(mission) - def pull_random_mission(self, world: World, slot: 'SC2MOGenMission', *, prefer_min_difficulty: Difficulty = None) -> SC2Mission: + def pull_random_mission(self, world: World, slot: 'SC2MOGenMission', *, prefer_close_difficulty: bool = False) -> SC2Mission: """Picks a random mission from the mission pool of the given slot and marks it as present in the mission order. With `prefer_close_difficulty = True` the mission is picked to be as close to the slot's desired difficulty as possible.""" pool = slot.option_mission_pool.intersection(self.master_list) - + difficulty_pools: Dict[int, List[int]] = { diff: sorted(pool.intersection(self.difficulty_pools[diff])) for diff in Difficulty if diff != Difficulty.RELATIVE @@ -195,13 +195,13 @@ def pull_random_mission(self, world: World, slot: 'SC2MOGenMission', *, prefer_m final_pool: List[int] = [] desired_difficulty = slot.option_difficulty - if prefer_min_difficulty is None: + if prefer_close_difficulty: # Iteratively look down and up around the slot's desired difficulty # Either a difficulty with valid missions is found, or an error is raised difficulty_offset = 0 while len(final_pool) == 0: lower_diff = max(desired_difficulty - difficulty_offset, 1) - higher_diff = min(desired_difficulty + difficulty_offset, 5) + higher_diff = min(desired_difficulty + difficulty_offset + 1, 5) final_pool = difficulty_pools[lower_diff] if len(final_pool) > 0: break @@ -211,25 +211,17 @@ def pull_random_mission(self, world: World, slot: 'SC2MOGenMission', *, prefer_m if lower_diff == Difficulty.STARTER and higher_diff == Difficulty.VERY_HARD: raise IndexError() difficulty_offset += 1 - + else: # Consider missions from all lower difficulties as well the desired difficulty - min_difficulty = prefer_min_difficulty - while len(final_pool) == 0: - final_pool = [ - mission - for difficulty in range(min_difficulty, desired_difficulty + 1) - for mission in difficulty_pools[difficulty] - ] - if min_difficulty <= Difficulty.STARTER: - break - min_difficulty -= 1 # Only take from higher difficulties if no lower difficulty is possible + final_pool = [ + mission + for difficulty in range(Difficulty.STARTER, desired_difficulty + 1) + for mission in difficulty_pools[difficulty] + ] difficulty_offset = 1 while len(final_pool) == 0: - # Ban difficulty offsets greater than 1 when reverse filling to avoid unnatural difficulty spikes - if difficulty_offset > 1 and prefer_min_difficulty == desired_difficulty: - raise Exception("Invalid mission pool for this mission order. Try changing your Mission Bias option to easy.") higher_difficulty = desired_difficulty + difficulty_offset if higher_difficulty > Difficulty.VERY_HARD: raise IndexError() diff --git a/worlds/sc2/mission_order/structs.py b/worlds/sc2/mission_order/structs.py index 84498a7b7280..55c21ca75454 100644 --- a/worlds/sc2/mission_order/structs.py +++ b/worlds/sc2/mission_order/structs.py @@ -469,7 +469,7 @@ def fill_missions( # Pick goal missions first with stricter difficulty matching, and starting with harder goals for goal_slot in sorted_goals: try: - mission = self.mission_pools.pull_random_mission(world, goal_slot) + mission = self.mission_pools.pull_random_mission(world, goal_slot, prefer_close_difficulty=True) goal_slot.set_mission(world, mission, locations_per_region, location_cache) regions.append(goal_slot.region) all_slots.remove(goal_slot) @@ -483,8 +483,7 @@ def fill_missions( remaining_count = len(all_slots) for mission_slot in all_slots: try: - prefer_min_difficulty = Difficulty.STARTER if prefer_easy_missions else mission_slot.option_difficulty - mission = self.mission_pools.pull_random_mission(world, mission_slot, prefer_min_difficulty=prefer_min_difficulty) + mission = self.mission_pools.pull_random_mission(world, mission_slot, prefer_close_difficulty=not prefer_easy_missions) mission_slot.set_mission(world, mission, locations_per_region, location_cache) regions.append(mission_slot.region) remaining_count -= 1 From 06e424d59b7ad6fac557ab86fb2f798e364f34be Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 25 Dec 2024 21:25:10 -0800 Subject: [PATCH 4/9] sc2: Adding new logic level between advanced and no logic --- worlds/sc2/locations.py | 53 +++++++++++++++++++---------- worlds/sc2/mission_order/options.py | 7 +++- worlds/sc2/mission_order/structs.py | 25 ++++++++++++-- worlds/sc2/options.py | 11 +++--- worlds/sc2/pool_filter.py | 3 ++ worlds/sc2/rules.py | 41 ++++++++++++++++++++-- 6 files changed, 111 insertions(+), 29 deletions(-) diff --git a/worlds/sc2/locations.py b/worlds/sc2/locations.py index f48b2c094ef1..edd81ad2ca0d 100644 --- a/worlds/sc2/locations.py +++ b/worlds/sc2/locations.py @@ -58,6 +58,7 @@ class LocationData(NamedTuple): type: LocationType rule: Callable[['CollectionState'], bool] = Location.access_rule flags: LocationFlag = LocationFlag.NONE + hard_rule: Optional[Callable[['CollectionState'], bool]] = None def make_location_data( @@ -67,8 +68,9 @@ def make_location_data( type: LocationType, rule: Callable[['CollectionState'], bool] = Location.access_rule, flags: LocationFlag = LocationFlag.NONE, + hard_rule: Optional[Callable[['CollectionState'], bool]] = None, ) -> LocationData: - return LocationData(region, f'{region}: {name}', code, type, rule, flags) + return LocationData(region, f'{region}: {name}', code, type, rule, flags, hard_rule) def get_location_types(world: 'SC2World', inclusion_type: int) -> Set[LocationType]: @@ -106,7 +108,6 @@ def get_location_flags(world: 'SC2World', inclusion_type: int) -> LocationFlag: def get_plando_locations(world: World) -> List[str]: """ - :param multiworld: :param player: :return: A list of locations affected by a plando in a world @@ -1316,28 +1317,36 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: and logic.zerg_competent_anti_air(state)) ), make_location_data(SC2Mission.SUPREME.mission_name, "Victory", SC2HOTS_LOC_ID_OFFSET + 1200, LocationType.VICTORY, - logic.supreme_requirement + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, ), make_location_data(SC2Mission.SUPREME.mission_name, "First Relic", SC2HOTS_LOC_ID_OFFSET + 1201, LocationType.VANILLA, - logic.supreme_requirement + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, ), make_location_data(SC2Mission.SUPREME.mission_name, "Second Relic", SC2HOTS_LOC_ID_OFFSET + 1202, LocationType.VANILLA, - logic.supreme_requirement + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, ), make_location_data(SC2Mission.SUPREME.mission_name, "Third Relic", SC2HOTS_LOC_ID_OFFSET + 1203, LocationType.VANILLA, - logic.supreme_requirement + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, ), make_location_data(SC2Mission.SUPREME.mission_name, "Fourth Relic", SC2HOTS_LOC_ID_OFFSET + 1204, LocationType.VANILLA, - logic.supreme_requirement + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, ), make_location_data(SC2Mission.SUPREME.mission_name, "Yagdra", SC2HOTS_LOC_ID_OFFSET + 1205, LocationType.EXTRA, - logic.supreme_requirement + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, ), make_location_data(SC2Mission.SUPREME.mission_name, "Kraith", SC2HOTS_LOC_ID_OFFSET + 1206, LocationType.EXTRA, - logic.supreme_requirement + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, ), make_location_data(SC2Mission.SUPREME.mission_name, "Slivan", SC2HOTS_LOC_ID_OFFSET + 1207, LocationType.EXTRA, - logic.supreme_requirement + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, ), make_location_data(SC2Mission.INFESTED.mission_name, "Victory", SC2HOTS_LOC_ID_OFFSET + 1300, LocationType.VICTORY, lambda state: ( @@ -2082,35 +2091,43 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.sudden_strike_requirement ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "Victory", SC2NCO_LOC_ID_OFFSET + 300, LocationType.VICTORY, - logic.enemy_intelligence_third_stage_requirement + logic.enemy_intelligence_third_stage_requirement, + hard_rule=logic.enemy_intelligence_cliff_garrison, ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "West Garrison", SC2NCO_LOC_ID_OFFSET + 301, LocationType.EXTRA, - logic.enemy_intelligence_first_stage_requirement + logic.enemy_intelligence_first_stage_requirement, + hard_rule=logic.enemy_intelligence_garrisonable_unit, ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "Close Garrison", SC2NCO_LOC_ID_OFFSET + 302, LocationType.EXTRA, - logic.enemy_intelligence_first_stage_requirement + logic.enemy_intelligence_first_stage_requirement, + hard_rule=logic.enemy_intelligence_garrisonable_unit, ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "Northeast Garrison", SC2NCO_LOC_ID_OFFSET + 303, LocationType.EXTRA, - logic.enemy_intelligence_first_stage_requirement + logic.enemy_intelligence_first_stage_requirement, + hard_rule=logic.enemy_intelligence_garrisonable_unit, ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "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)) + and logic.enemy_intelligence_cliff_garrison(state)), + hard_rule=logic.enemy_intelligence_cliff_garrison, ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "South Garrison", SC2NCO_LOC_ID_OFFSET + 305, LocationType.EXTRA, - logic.enemy_intelligence_first_stage_requirement + logic.enemy_intelligence_first_stage_requirement, + hard_rule=logic.enemy_intelligence_garrisonable_unit, ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "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)) + and logic.enemy_intelligence_cliff_garrison(state)), + hard_rule=logic.enemy_intelligence_cliff_garrison, ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "Forces Rescued", SC2NCO_LOC_ID_OFFSET + 307, LocationType.VANILLA, logic.enemy_intelligence_first_stage_requirement ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "Communications Hub", SC2NCO_LOC_ID_OFFSET + 308, LocationType.VANILLA, - logic.enemy_intelligence_second_stage_requirement + logic.enemy_intelligence_second_stage_requirement, + hard_rule=logic.enemy_intelligence_cliff_garrison, ), make_location_data(SC2Mission.TROUBLE_IN_PARADISE.mission_name, "Victory", SC2NCO_LOC_ID_OFFSET + 400, LocationType.VICTORY, logic.trouble_in_paradise_requirement diff --git a/worlds/sc2/mission_order/options.py b/worlds/sc2/mission_order/options.py index b3b3c576edf8..bfe7a8c17f84 100644 --- a/worlds/sc2/mission_order/options.py +++ b/worlds/sc2/mission_order/options.py @@ -11,7 +11,8 @@ from ..mission_groups import mission_groups from ..item.item_tables import item_table from ..item.item_groups import item_name_groups -from .structs import Difficulty, LayoutType, GENERIC_KEY_NAME +from .layout_types import LayoutType +from .mission_pools import Difficulty from .layout_types import Column, Grid, Hopscotch, Gauntlet, Blitz, Canvas from .presets_static import ( static_preset, preset_mini_wol_with_prophecy, preset_mini_wol, preset_mini_hots, preset_mini_prophecy, @@ -21,6 +22,10 @@ ) from .presets_scripted import make_golden_path + +GENERIC_KEY_NAME = "Key".casefold() + + STR_OPTION_VALUES: Dict[str, Dict[str, Any]] = { "type": { "column": Column, "grid": Grid, "hopscotch": Hopscotch, "gauntlet": Gauntlet, "blitz": Blitz, diff --git a/worlds/sc2/mission_order/structs.py b/worlds/sc2/mission_order/structs.py index 55c21ca75454..cf93c0a87e91 100644 --- a/worlds/sc2/mission_order/structs.py +++ b/worlds/sc2/mission_order/structs.py @@ -6,18 +6,19 @@ import logging from BaseClasses import Region, Location, CollectionState, Entrance -from ..mission_tables import SC2Mission, lookup_name_to_mission, MissionFlag, lookup_id_to_mission, get_goal_location +from ..mission_tables import SC2Mission, SC2Race, lookup_name_to_mission, MissionFlag, lookup_id_to_mission, get_goal_location from ..item.item_tables import named_layout_key_item_table, named_campaign_key_item_table from ..item import item_names from .layout_types import LayoutType from .entry_rules import EntryRule, SubRuleEntryRule, CountMissionsEntryRule, BeatMissionsEntryRule, SubRuleRuleData, ItemEntryRule from .mission_pools import SC2MOGenMissionPools, Difficulty, modified_difficulty_thresholds +from .options import GENERIC_KEY_NAME +from .. import rules if TYPE_CHECKING: from ..locations import LocationData from .. import SC2World -GENERIC_KEY_NAME = "Key".casefold() class MissionOrderNode(ABC): parent: Optional[ReferenceType[MissionOrderNode]] @@ -1069,7 +1070,22 @@ def create_location(player: int, location_data: 'LocationData', region: Region, location.access_rule = location_data.rule location_cache.append(location) + return location + +def create_minimal_logic_location( + player: int, location_data: 'LocationData', region: Region, location_cache: List[Location], unit_count: int = 0, +) -> Location: + location = Location(player, location_data.name, location_data.code, region) + mission = lookup_name_to_mission.get(region.name) + if mission is None: + pass + elif location_data.hard_rule: + unit_rule = rules.has_race_units(player, unit_count, mission.race) + location.access_rule = lambda state: unit_rule(state) and location_data.hard_rule(state) + else: + location.access_rule = rules.has_race_units(player, unit_count, mission.race) + location_cache.append(location) return location @@ -1094,7 +1110,10 @@ def create_region( if victory_cache_locations >= target_victory_cache_locations: continue victory_cache_locations += 1 - location = create_location(world.player, location_data, region, location_cache) + if world.options.required_tactics.value == world.options.required_tactics.option_any_units: + location = create_minimal_logic_location(world.player, location_data, region, location_cache, min(slot.min_depth, 5)) + else: + location = create_location(world.player, location_data, region, location_cache) region.locations.append(location) return region diff --git a/worlds/sc2/options.py b/worlds/sc2/options.py index 29f28c66a7c9..bb93d2d68a99 100644 --- a/worlds/sc2/options.py +++ b/worlds/sc2/options.py @@ -365,18 +365,21 @@ class StarterUnit(Choice): class RequiredTactics(Choice): """ - Determines the maximum tactical difficulty of the world (separate from mission difficulty). Higher settings - increase randomness. + Determines the maximum tactical difficulty of the world (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 micro-heavy units. - No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES! + Any Units: Logic guarantees at least one unit or building unlock per mission depth, up to 5 units, + without restriction on what those units are. May render the run impossible on harder difficulties. + No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES! Locks Grant Story Tech option to true. """ display_name = "Required Tactics" option_standard = 0 option_advanced = 1 - option_no_logic = 2 + option_any_units = 2 + option_no_logic = 3 class EnableVoidTrade(Toggle): diff --git a/worlds/sc2/pool_filter.py b/worlds/sc2/pool_filter.py index a683f2727d32..4695daa65b63 100644 --- a/worlds/sc2/pool_filter.py +++ b/worlds/sc2/pool_filter.py @@ -145,6 +145,9 @@ def count(self, item: str, player: int) -> int: def count_from_list(self, items: Iterable[str], player: int) -> int: return sum(self.logical_inventory.get(item, 0) for item in items) + def count_from_list_unique(self, items: Iterable[str], player: int) -> int: + return sum(item in self.logical_inventory for item in items) + def generate_reduced_inventory(self, inventory_size: int, filler_amount: int, mission_requirements: List[Tuple[str, Callable]]) -> List[StarcraftItem]: """Attempts to generate a reduced inventory that can fulfill the mission requirements.""" inventory: List[StarcraftItem] = list(self.item_pool) diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py index 69c6c7d825b0..7a530ddf14fb 100644 --- a/worlds/sc2/rules.py +++ b/worlds/sc2/rules.py @@ -1,10 +1,10 @@ from math import floor -from typing import TYPE_CHECKING, Set, Optional +from typing import TYPE_CHECKING, Set, Optional, Callable -from BaseClasses import CollectionState +from BaseClasses import CollectionState, Location from .options import ( get_option_value, RequiredTactics, kerrigan_unit_available, AllInMap, - GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence, + GrantStoryTech, GrantStoryLevels, SpearOfAdunAutonomouslyCastAbilityPresence, get_enabled_campaigns, MissionOrder, EnableMorphling, get_enabled_races ) from .item.item_tables import ( @@ -2399,6 +2399,7 @@ def __init__(self, world: Optional['SC2World']): self.mission_order = get_option_value(world, "mission_order") self.generic_upgrade_missions = get_option_value(world, "generic_upgrade_missions") + def get_basic_units(logic_level: int, race: SC2Race) -> Set[str]: if logic_level == RequiredTactics.option_no_logic: return no_logic_basic_units[race] @@ -2406,3 +2407,37 @@ def get_basic_units(logic_level: int, race: SC2Race) -> Set[str]: return advanced_basic_units[race] else: return basic_units[race] + + +def has_terran_units(player: int, target: int) -> Callable[['CollectionState'], bool]: + def _has_terran_units(state: CollectionState) -> bool: + return ( + state.count_from_list_unique(item_groups.terran_units + item_groups.terran_buildings, player) >= target + ) + return _has_terran_units + + +def has_zerg_units(player: int, target: int) -> Callable[['CollectionState'], bool]: + def _has_zerg_units(state: CollectionState) -> bool: + return ( + state.count_from_list_unique(item_groups.zerg_units + item_groups.zerg_buildings, player) >= target + ) + return _has_zerg_units + + +def has_protoss_units(player: int, target: int) -> Callable[['CollectionState'], bool]: + def _has_protoss_units(state: CollectionState) -> bool: + return ( + state.count_from_list_unique(item_groups.protoss_units + item_groups.protoss_buildings, player) >= target + ) + return _has_protoss_units + + +def has_race_units(player: int, target: int, race: SC2Race) -> Callable[['CollectionState'], bool]: + if race == SC2Race.TERRAN: + return has_terran_units(player, target) + if race == SC2Race.ZERG: + return has_zerg_units(player, target) + if race == SC2Race.PROTOSS: + return has_protoss_units(player, target) + return Location.access_rule From 0b265b71f5bafe8394b88d533b1ea5ea12fda21c Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 26 Dec 2024 18:49:03 -0800 Subject: [PATCH 5/9] sc2: Updating description for any units logic level --- worlds/sc2/options.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/sc2/options.py b/worlds/sc2/options.py index bb93d2d68a99..829e200b148c 100644 --- a/worlds/sc2/options.py +++ b/worlds/sc2/options.py @@ -370,8 +370,10 @@ class RequiredTactics(Choice): Standard: All missions can be completed with good micro and macro. Advanced: Completing missions may require relying on starting units and micro-heavy units. - Any Units: Logic guarantees at least one unit or building unlock per mission depth, up to 5 units, - without restriction on what those units are. May render the run impossible on harder difficulties. + Any Units: Logic guarantees faction-appropriate units appear early without regard to what those units are. + i.e. if the third mission is a protoss build mission, + logic guarantees at least 2 protoss units are reachable before starting it. + May render the run impossible on harder difficulties. No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES! Locks Grant Story Tech option to true. """ From b8a65f122017bfa87baa11e43c8a2de1590948b3 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 27 Dec 2024 15:23:52 -0800 Subject: [PATCH 6/9] sc2: Adding more hard rules for Nova no-builds and strict anti-air requirements (motherships, leviathan, slayn elementals) --- worlds/sc2/item/item_tables.py | 21 +-- worlds/sc2/locations.py | 261 +++++++++++++++++++++------------ worlds/sc2/rules.py | 154 ++++++++++++++++++- 3 files changed, 328 insertions(+), 108 deletions(-) diff --git a/worlds/sc2/item/item_tables.py b/worlds/sc2/item/item_tables.py index 3d4613f6c924..45a7a1f90955 100644 --- a/worlds/sc2/item/item_tables.py +++ b/worlds/sc2/item/item_tables.py @@ -655,7 +655,7 @@ def get_full_item_list(): classification=ItemClassification.progression, parent=item_names.LIBERATOR), item_names.WIDOW_MINE_DRILLING_CLAWS: ItemData(328 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 9, SC2Race.TERRAN, - classification=ItemClassification.filler, parent=item_names.WIDOW_MINE), + parent=item_names.WIDOW_MINE), item_names.WIDOW_MINE_CONCEALMENT: ItemData(329 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 10, SC2Race.TERRAN, classification=ItemClassification.progression, parent=item_names.WIDOW_MINE), @@ -917,7 +917,8 @@ def get_full_item_list(): ItemData(512 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 12, SC2Race.TERRAN, classification=ItemClassification.progression), item_names.JOTUN: - ItemData(513 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 13, SC2Race.TERRAN), + ItemData(513 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 13, SC2Race.TERRAN, + classification=ItemClassification.progression), item_names.ULTRA_CAPACITORS: ItemData(600 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 0, SC2Race.TERRAN), @@ -1674,7 +1675,7 @@ def get_full_item_list(): classification=ItemClassification.progression), item_names.SUPPLICANT: ItemData(3 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 12, SC2Race.PROTOSS, - classification=ItemClassification.filler, important_for_filtering=True), + classification=ItemClassification.progression), item_names.INSTIGATOR: ItemData(4 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 13, SC2Race.PROTOSS, classification=ItemClassification.progression), @@ -1891,7 +1892,7 @@ def get_full_item_list(): item_names.ARCHON_POWER_SIPHON: ItemData(392 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 2, SC2Race.PROTOSS, parent=parent_names.ARCHON_SOURCE), item_names.ARCHON_ERADICATE: ItemData(393 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 3, SC2Race.PROTOSS, parent=parent_names.ARCHON_SOURCE), item_names.ARCHON_OBLITERATE: ItemData(394 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 4, SC2Race.PROTOSS, parent=parent_names.ARCHON_SOURCE), - item_names.SUPPLICANT_ZENITH_PITCH: ItemData(395 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 5, SC2Race.PROTOSS, parent=item_names.SUPPLICANT), + item_names.SUPPLICANT_ZENITH_PITCH: ItemData(395 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 5, SC2Race.PROTOSS, parent=item_names.SUPPLICANT, classification=ItemClassification.progression), # War Council item_names.ZEALOT_WHIRLWIND: ItemData(500 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 0, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.ZEALOT), @@ -1916,7 +1917,7 @@ def get_full_item_list(): item_names.IMMORTAL_IMPROVED_BARRIER: ItemData(519 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 19, SC2Race.PROTOSS, parent=item_names.IMMORTAL), item_names.VANGUARD_RAPIDFIRE_CANNON: ItemData(520 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 20, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.VANGUARD), item_names.VANGUARD_FUSION_MORTARS: ItemData(521 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 21, SC2Race.PROTOSS, parent=item_names.VANGUARD), - item_names.ANNIHILATOR_AERIAL_TRACKING: ItemData(522 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 22, SC2Race.PROTOSS, parent=item_names.ANNIHILATOR), + item_names.ANNIHILATOR_AERIAL_TRACKING: ItemData(522 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 22, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.ANNIHILATOR), item_names.STALWART_ARC_INDUCERS: ItemData(523 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 23, SC2Race.PROTOSS, parent=item_names.STALWART), item_names.COLOSSUS_FIRE_LANCE: ItemData(524 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 24, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.COLOSSUS), item_names.WRATHWALKER_AERIAL_TRACKING: ItemData(525 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 25, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.WRATHWALKER), @@ -1943,17 +1944,17 @@ def get_full_item_list(): # SoA Calldown powers item_names.SOA_CHRONO_SURGE: ItemData(700 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 0, SC2Race.PROTOSS), - item_names.SOA_PROGRESSIVE_PROXY_PYLON: ItemData(701 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Progressive, 0, SC2Race.PROTOSS, quantity=2), - item_names.SOA_PYLON_OVERCHARGE: ItemData(702 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 1, SC2Race.PROTOSS), - item_names.SOA_ORBITAL_STRIKE: ItemData(703 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 2, SC2Race.PROTOSS), + item_names.SOA_PROGRESSIVE_PROXY_PYLON: ItemData(701 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Progressive, 0, SC2Race.PROTOSS, quantity=2, classification=ItemClassification.progression), + item_names.SOA_PYLON_OVERCHARGE: ItemData(702 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 1, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.SOA_ORBITAL_STRIKE: ItemData(703 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 2, SC2Race.PROTOSS, classification=ItemClassification.progression), item_names.SOA_TEMPORAL_FIELD: ItemData(704 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 3, SC2Race.PROTOSS), item_names.SOA_SOLAR_LANCE: ItemData(705 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 4, SC2Race.PROTOSS, classification=ItemClassification.progression), item_names.SOA_MASS_RECALL: ItemData(706 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 5, SC2Race.PROTOSS), item_names.SOA_SHIELD_OVERCHARGE: ItemData(707 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 6, SC2Race.PROTOSS), item_names.SOA_DEPLOY_FENIX: ItemData(708 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 7, SC2Race.PROTOSS, classification=ItemClassification.progression), - item_names.SOA_PURIFIER_BEAM: ItemData(709 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 8, SC2Race.PROTOSS), + item_names.SOA_PURIFIER_BEAM: ItemData(709 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 8, SC2Race.PROTOSS, classification=ItemClassification.progression), item_names.SOA_TIME_STOP: ItemData(710 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 9, SC2Race.PROTOSS, classification=ItemClassification.progression), - item_names.SOA_SOLAR_BOMBARDMENT: ItemData(711 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 10, SC2Race.PROTOSS), + item_names.SOA_SOLAR_BOMBARDMENT: ItemData(711 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 10, SC2Race.PROTOSS, classification=ItemClassification.progression), # Generic Protoss Upgrades item_names.MATRIX_OVERLOAD: diff --git a/worlds/sc2/locations.py b/worlds/sc2/locations.py index edd81ad2ca0d..7203bc94c9c5 100644 --- a/worlds/sc2/locations.py +++ b/worlds/sc2/locations.py @@ -254,7 +254,8 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.terran_outbreak_requirement ), make_location_data(SC2Mission.SAFE_HAVEN.mission_name, "Victory", SC2WOL_LOC_ID_OFFSET + 600, LocationType.VICTORY, - logic.terran_safe_haven_requirement + logic.terran_safe_haven_requirement, + hard_rule=logic.terran_any_anti_air, ), make_location_data(SC2Mission.SAFE_HAVEN.mission_name, "North Nexus", SC2WOL_LOC_ID_OFFSET + 601, LocationType.EXTRA, logic.terran_safe_haven_requirement @@ -266,13 +267,16 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.terran_safe_haven_requirement ), make_location_data(SC2Mission.SAFE_HAVEN.mission_name, "First Terror Fleet", SC2WOL_LOC_ID_OFFSET + 604, LocationType.VANILLA, - logic.terran_safe_haven_requirement + logic.terran_safe_haven_requirement, + hard_rule=logic.terran_any_anti_air, ), make_location_data(SC2Mission.SAFE_HAVEN.mission_name, "Second Terror Fleet", SC2WOL_LOC_ID_OFFSET + 605, LocationType.VANILLA, - logic.terran_safe_haven_requirement + logic.terran_safe_haven_requirement, + hard_rule=logic.terran_any_anti_air, ), make_location_data(SC2Mission.SAFE_HAVEN.mission_name, "Third Terror Fleet", SC2WOL_LOC_ID_OFFSET + 606, LocationType.VANILLA, - logic.terran_safe_haven_requirement + logic.terran_safe_haven_requirement, + hard_rule=logic.terran_any_anti_air, ), make_location_data(SC2Mission.HAVENS_FALL.mission_name, "Victory", SC2WOL_LOC_ID_OFFSET + 700, LocationType.VICTORY, lambda state: ( @@ -533,7 +537,8 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.terran_survives_rip_field ), make_location_data(SC2Mission.MAW_OF_THE_VOID.mission_name, "Mothership", SC2WOL_LOC_ID_OFFSET + 1206, LocationType.EXTRA, - logic.terran_survives_rip_field + logic.terran_survives_rip_field, + hard_rule=logic.terran_any_anti_air, ), make_location_data(SC2Mission.MAW_OF_THE_VOID.mission_name, "Expansion Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1207, LocationType.EXTRA, lambda state: adv_tactics or logic.terran_survives_rip_field(state) @@ -938,7 +943,8 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.terran_competent_comp ), make_location_data(SC2Mission.SHATTER_THE_SKY.mission_name, "Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, LocationType.VANILLA, - logic.terran_competent_comp + logic.terran_competent_comp, + hard_rule=logic.terran_any_anti_air, ), make_location_data(SC2Mission.SHATTER_THE_SKY.mission_name, "East Hatchery", SC2WOL_LOC_ID_OFFSET + 2806, LocationType.EXTRA, logic.terran_competent_comp @@ -1798,7 +1804,8 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.protoss_common_unit_anti_armor_air ), make_location_data(SC2Mission.TEMPLE_OF_UNIFICATION.mission_name, "Titanic Warp Prism", SC2LOTV_LOC_ID_OFFSET + 1206, LocationType.VANILLA, - logic.protoss_common_unit_anti_armor_air + logic.protoss_common_unit_anti_armor_air, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, ), make_location_data(SC2Mission.THE_INFINITE_CYCLE.mission_name, "Victory", SC2LOTV_LOC_ID_OFFSET + 1300, LocationType.VICTORY, logic.the_infinite_cycle_requirement @@ -1940,22 +1947,27 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.protoss_steps_of_the_rite_requirement ), make_location_data(SC2Mission.STEPS_OF_THE_RITE.mission_name, "North Mothership", SC2LOTV_LOC_ID_OFFSET + 1706, LocationType.VANILLA, - logic.protoss_steps_of_the_rite_requirement + logic.protoss_steps_of_the_rite_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, ), make_location_data(SC2Mission.STEPS_OF_THE_RITE.mission_name, "South Mothership", SC2LOTV_LOC_ID_OFFSET + 1707, LocationType.VANILLA, - logic.protoss_steps_of_the_rite_requirement + logic.protoss_steps_of_the_rite_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, ), make_location_data(SC2Mission.RAK_SHIR.mission_name, "Victory", SC2LOTV_LOC_ID_OFFSET + 1800, LocationType.VICTORY, logic.protoss_competent_comp ), make_location_data(SC2Mission.RAK_SHIR.mission_name, "North Slayn Elemental", SC2LOTV_LOC_ID_OFFSET + 1801, LocationType.VANILLA, - logic.protoss_competent_comp + logic.protoss_competent_comp, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, ), make_location_data(SC2Mission.RAK_SHIR.mission_name, "Southwest Slayn Elemental", SC2LOTV_LOC_ID_OFFSET + 1802, LocationType.VANILLA, - logic.protoss_competent_comp + logic.protoss_competent_comp, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, ), make_location_data(SC2Mission.RAK_SHIR.mission_name, "East Slayn Elemental", SC2LOTV_LOC_ID_OFFSET + 1803, LocationType.VANILLA, - logic.protoss_competent_comp + logic.protoss_competent_comp, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, ), make_location_data(SC2Mission.TEMPLAR_S_CHARGE.mission_name, "Victory", SC2LOTV_LOC_ID_OFFSET + 1900, LocationType.VICTORY, logic.protoss_templars_charge_requirement @@ -2061,22 +2073,27 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: # Nova Covert Ops make_location_data(SC2Mission.THE_ESCAPE.mission_name, "Victory", SC2NCO_LOC_ID_OFFSET + 100, LocationType.VICTORY, - logic.the_escape_requirement + logic.the_escape_requirement, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.THE_ESCAPE.mission_name, "Rifle", SC2NCO_LOC_ID_OFFSET + 101, LocationType.VANILLA, logic.the_escape_first_stage_requirement ), make_location_data(SC2Mission.THE_ESCAPE.mission_name, "Grenades", SC2NCO_LOC_ID_OFFSET + 102, LocationType.VANILLA, - logic.the_escape_first_stage_requirement + logic.the_escape_first_stage_requirement, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.THE_ESCAPE.mission_name, "Agent Delta", SC2NCO_LOC_ID_OFFSET + 103, LocationType.VANILLA, - logic.the_escape_requirement + logic.the_escape_requirement, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.THE_ESCAPE.mission_name, "Agent Pierce", SC2NCO_LOC_ID_OFFSET + 104, LocationType.VANILLA, - logic.the_escape_requirement + logic.the_escape_requirement, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.THE_ESCAPE.mission_name, "Agent Stone", SC2NCO_LOC_ID_OFFSET + 105, LocationType.VANILLA, - logic.the_escape_requirement + logic.the_escape_requirement, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.SUDDEN_STRIKE.mission_name, "Victory", SC2NCO_LOC_ID_OFFSET + 200, LocationType.VICTORY, logic.sudden_strike_requirement @@ -2092,7 +2109,7 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "Victory", SC2NCO_LOC_ID_OFFSET + 300, LocationType.VICTORY, logic.enemy_intelligence_third_stage_requirement, - hard_rule=logic.enemy_intelligence_cliff_garrison, + hard_rule=logic.enemy_intelligence_cliff_garrison_and_nova_mobility, ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "West Garrison", SC2NCO_LOC_ID_OFFSET + 301, LocationType.EXTRA, logic.enemy_intelligence_first_stage_requirement, @@ -2127,7 +2144,7 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: ), make_location_data(SC2Mission.ENEMY_INTELLIGENCE.mission_name, "Communications Hub", SC2NCO_LOC_ID_OFFSET + 308, LocationType.VANILLA, logic.enemy_intelligence_second_stage_requirement, - hard_rule=logic.enemy_intelligence_cliff_garrison, + hard_rule=logic.enemy_intelligence_cliff_garrison_and_nova_mobility, ), make_location_data(SC2Mission.TROUBLE_IN_PARADISE.mission_name, "Victory", SC2NCO_LOC_ID_OFFSET + 400, LocationType.VICTORY, logic.trouble_in_paradise_requirement @@ -2260,22 +2277,28 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.flashpoint_far_requirement ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Victory", SC2NCO_LOC_ID_OFFSET + 700, LocationType.VICTORY, - logic.enemy_shadow_victory + logic.enemy_shadow_victory, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Sewers: Domination Visor", SC2NCO_LOC_ID_OFFSET + 701, LocationType.VANILLA, - logic.enemy_shadow_domination + logic.enemy_shadow_domination, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Sewers: Resupply Crate", SC2NCO_LOC_ID_OFFSET + 702, LocationType.EXTRA, - logic.enemy_shadow_first_stage + logic.enemy_shadow_first_stage, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Sewers: Facility Access", SC2NCO_LOC_ID_OFFSET + 703, LocationType.VANILLA, - logic.enemy_shadow_first_stage + logic.enemy_shadow_first_stage, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Facility: Northwest Door Lock", SC2NCO_LOC_ID_OFFSET + 704, LocationType.VANILLA, - logic.enemy_shadow_door_controls + logic.enemy_shadow_door_controls, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Facility: Southeast Door Lock", SC2NCO_LOC_ID_OFFSET + 705, LocationType.VANILLA, - logic.enemy_shadow_door_controls + logic.enemy_shadow_door_controls, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Facility: Blazefire Gunblade", SC2NCO_LOC_ID_OFFSET + 706, LocationType.VANILLA, lambda state: ( @@ -2285,28 +2308,36 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: or (adv_tactics and state.has_all({item_names.NOVA_DOMINATION, item_names.NOVA_HOLO_DECOY, item_names.NOVA_JUMP_SUIT_MODULE}, player) ) - )) + )), + hard_rule=logic.enemy_shadow_nova_damage_and_blazefire_unlock, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Facility: Blink Suit", SC2NCO_LOC_ID_OFFSET + 707, LocationType.VANILLA, - logic.enemy_shadow_second_stage + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Facility: Advanced Weaponry", SC2NCO_LOC_ID_OFFSET + 708, LocationType.VANILLA, - logic.enemy_shadow_second_stage + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Facility: Entrance Resupply Crate", SC2NCO_LOC_ID_OFFSET + 709, LocationType.EXTRA, - logic.enemy_shadow_first_stage + logic.enemy_shadow_first_stage, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Facility: West Resupply Crate", SC2NCO_LOC_ID_OFFSET + 710, LocationType.EXTRA, - logic.enemy_shadow_second_stage + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Facility: North Resupply Crate", SC2NCO_LOC_ID_OFFSET + 711, LocationType.EXTRA, - logic.enemy_shadow_second_stage + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Facility: East Resupply Crate", SC2NCO_LOC_ID_OFFSET + 712, LocationType.EXTRA, - logic.enemy_shadow_second_stage + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, "Facility: South Resupply Crate", SC2NCO_LOC_ID_OFFSET + 713, LocationType.EXTRA, - logic.enemy_shadow_second_stage + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, ), make_location_data(SC2Mission.DARK_SKIES.mission_name, "Victory", SC2NCO_LOC_ID_OFFSET + 800, LocationType.VICTORY, logic.dark_skies_requirement @@ -2559,47 +2590,69 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: and logic.protoss_common_unit(state)) ), make_location_data(SC2Mission.SAFE_HAVEN_Z.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 1100, LocationType.VICTORY, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), + lambda state: logic.zerg_common_unit(state) and + logic.zerg_competent_anti_air(state), + hard_rule=logic.zerg_any_anti_air, + ), make_location_data(SC2Mission.SAFE_HAVEN_Z.mission_name, "North Nexus", SC2_RACESWAP_LOC_ID_OFFSET + 1101, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), + lambda state: logic.zerg_common_unit(state) and + logic.zerg_competent_anti_air(state) + ), make_location_data(SC2Mission.SAFE_HAVEN_Z.mission_name, "East Nexus", SC2_RACESWAP_LOC_ID_OFFSET + 1102, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), + lambda state: logic.zerg_common_unit(state) and + logic.zerg_competent_anti_air(state) + ), make_location_data(SC2Mission.SAFE_HAVEN_Z.mission_name, "South Nexus", SC2_RACESWAP_LOC_ID_OFFSET + 1103, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), + lambda state: logic.zerg_common_unit(state) and + logic.zerg_competent_anti_air(state) + ), make_location_data(SC2Mission.SAFE_HAVEN_Z.mission_name, "First Terror Fleet", SC2_RACESWAP_LOC_ID_OFFSET + 1104, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), + lambda state: logic.zerg_common_unit(state) and + logic.zerg_competent_anti_air(state), + hard_rule=logic.zerg_any_anti_air, + ), make_location_data(SC2Mission.SAFE_HAVEN_Z.mission_name, "Second Terror Fleet", SC2_RACESWAP_LOC_ID_OFFSET + 1105, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), + lambda state: logic.zerg_common_unit(state) and + logic.zerg_competent_anti_air(state), + hard_rule=logic.zerg_any_anti_air, + ), make_location_data(SC2Mission.SAFE_HAVEN_Z.mission_name, "Third Terror Fleet", SC2_RACESWAP_LOC_ID_OFFSET + 1106, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), + lambda state: logic.zerg_common_unit(state) and + logic.zerg_competent_anti_air(state), + hard_rule=logic.zerg_any_anti_air, + ), make_location_data(SC2Mission.SAFE_HAVEN_P.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 1200, LocationType.VICTORY, - lambda state: logic.protoss_common_unit(state) and - logic.protoss_competent_anti_air(state)), + lambda state: logic.protoss_common_unit(state) and + logic.protoss_competent_anti_air(state), + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), make_location_data(SC2Mission.SAFE_HAVEN_P.mission_name, "North Nexus", SC2_RACESWAP_LOC_ID_OFFSET + 1201, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) and - logic.protoss_competent_anti_air(state)), + lambda state: logic.protoss_common_unit(state) and + logic.protoss_competent_anti_air(state) + ), make_location_data(SC2Mission.SAFE_HAVEN_P.mission_name, "East Nexus", SC2_RACESWAP_LOC_ID_OFFSET + 1202, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) and - logic.protoss_competent_anti_air(state)), + lambda state: logic.protoss_common_unit(state) and + logic.protoss_competent_anti_air(state) + ), make_location_data(SC2Mission.SAFE_HAVEN_P.mission_name, "South Nexus", SC2_RACESWAP_LOC_ID_OFFSET + 1203, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) and - logic.protoss_competent_anti_air(state)), + lambda state: logic.protoss_common_unit(state) and + logic.protoss_competent_anti_air(state) + ), make_location_data(SC2Mission.SAFE_HAVEN_P.mission_name, "First Terror Fleet", SC2_RACESWAP_LOC_ID_OFFSET + 1204, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) and - logic.protoss_competent_anti_air(state)), + lambda state: logic.protoss_common_unit(state) and + logic.protoss_competent_anti_air(state), + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), make_location_data(SC2Mission.SAFE_HAVEN_P.mission_name, "Second Terror Fleet", SC2_RACESWAP_LOC_ID_OFFSET + 1205, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) and - logic.protoss_competent_anti_air(state)), + lambda state: logic.protoss_common_unit(state) and + logic.protoss_competent_anti_air(state), + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), make_location_data(SC2Mission.SAFE_HAVEN_P.mission_name, "Third Terror Fleet", SC2_RACESWAP_LOC_ID_OFFSET + 1206, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) and - logic.protoss_competent_anti_air(state)), + lambda state: logic.protoss_common_unit(state) and + logic.protoss_competent_anti_air(state), + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), make_location_data(SC2Mission.HAVENS_FALL_Z.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 1300, LocationType.VICTORY, lambda state: ( logic.zerg_common_unit(state) @@ -3115,7 +3168,8 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.zerg_maw_requirement ), make_location_data(SC2Mission.MAW_OF_THE_VOID_Z.mission_name, "Mothership", SC2_RACESWAP_LOC_ID_OFFSET + 2306, LocationType.EXTRA, - logic.zerg_maw_requirement + logic.zerg_maw_requirement, + hard_rule=logic.zerg_any_anti_air, ), make_location_data(SC2Mission.MAW_OF_THE_VOID_Z.mission_name, "Expansion Rip Field Generator", SC2_RACESWAP_LOC_ID_OFFSET + 2307, LocationType.EXTRA, logic.zerg_maw_requirement @@ -3155,7 +3209,8 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.protoss_maw_requirement ), make_location_data(SC2Mission.MAW_OF_THE_VOID_P.mission_name, "Mothership", SC2_RACESWAP_LOC_ID_OFFSET + 2406, LocationType.EXTRA, - logic.protoss_maw_requirement + logic.protoss_maw_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, ), make_location_data(SC2Mission.MAW_OF_THE_VOID_P.mission_name, "Expansion Rip Field Generator", SC2_RACESWAP_LOC_ID_OFFSET + 2407, LocationType.EXTRA, lambda state: adv_tactics or logic.protoss_maw_requirement(state) @@ -3838,7 +3893,8 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.zerg_competent_comp ), make_location_data(SC2Mission.SHATTER_THE_SKY_Z.mission_name, "Leviathan", SC2_RACESWAP_LOC_ID_OFFSET + 5505, LocationType.VANILLA, - logic.zerg_competent_comp + logic.zerg_competent_comp, + hard_rule=logic.zerg_any_anti_air, ), make_location_data(SC2Mission.SHATTER_THE_SKY_Z.mission_name, "East Hatchery", SC2_RACESWAP_LOC_ID_OFFSET + 5506, LocationType.EXTRA, logic.zerg_competent_comp @@ -3865,7 +3921,8 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.protoss_competent_comp ), make_location_data(SC2Mission.SHATTER_THE_SKY_P.mission_name, "Leviathan", SC2_RACESWAP_LOC_ID_OFFSET + 5605, LocationType.VANILLA, - logic.protoss_competent_comp + logic.protoss_competent_comp, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, ), make_location_data(SC2Mission.SHATTER_THE_SKY_P.mission_name, "East Hatchery", SC2_RACESWAP_LOC_ID_OFFSET + 5606, LocationType.EXTRA, logic.protoss_competent_comp @@ -5359,8 +5416,9 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.terran_beats_protoss_deathball ), make_location_data(SC2Mission.TEMPLE_OF_UNIFICATION_T.mission_name, "Titanic Warp Prism", SC2_RACESWAP_LOC_ID_OFFSET + 12106, LocationType.VANILLA, - logic.terran_beats_protoss_deathball - ), + logic.terran_beats_protoss_deathball, + hard_rule=logic.terran_any_anti_air, + ), make_location_data(SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 12200, LocationType.VICTORY, logic.zerg_temple_of_unification_requirement ), @@ -5380,8 +5438,9 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.zerg_temple_of_unification_requirement ), make_location_data(SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, "Titanic Warp Prism", SC2_RACESWAP_LOC_ID_OFFSET + 12206, LocationType.VANILLA, - logic.zerg_temple_of_unification_requirement - ), + logic.zerg_temple_of_unification_requirement, + hard_rule=logic.zerg_any_anti_air, + ), make_location_data(SC2Mission.HARBINGER_OF_OBLIVION_T.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 12500, LocationType.VICTORY, logic.terran_harbinger_of_oblivion_requirement ), @@ -5609,11 +5668,13 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.terran_steps_of_the_rite_requirement ), make_location_data(SC2Mission.STEPS_OF_THE_RITE_T.mission_name, "North Mothership", SC2_RACESWAP_LOC_ID_OFFSET + 13106, LocationType.VANILLA, - logic.terran_steps_of_the_rite_requirement - ), + logic.terran_steps_of_the_rite_requirement, + hard_rule=logic.terran_any_anti_air, + ), make_location_data(SC2Mission.STEPS_OF_THE_RITE_T.mission_name, "South Mothership", SC2_RACESWAP_LOC_ID_OFFSET + 13107, LocationType.VANILLA, - logic.terran_steps_of_the_rite_requirement - ), + logic.terran_steps_of_the_rite_requirement, + hard_rule=logic.terran_any_anti_air, + ), make_location_data(SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 13200, LocationType.VICTORY, logic.zerg_steps_of_the_rite_requirement ), @@ -5633,38 +5694,46 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: logic.zerg_steps_of_the_rite_requirement ), make_location_data(SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, "North Mothership", SC2_RACESWAP_LOC_ID_OFFSET + 13206, LocationType.VANILLA, - logic.zerg_steps_of_the_rite_requirement - ), + logic.zerg_steps_of_the_rite_requirement, + hard_rule=logic.zerg_any_anti_air, + ), make_location_data(SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, "South Mothership", SC2_RACESWAP_LOC_ID_OFFSET + 13207, LocationType.VANILLA, - logic.zerg_steps_of_the_rite_requirement - ), + logic.zerg_steps_of_the_rite_requirement, + hard_rule=logic.zerg_any_anti_air, + ), make_location_data(SC2Mission.RAK_SHIR_T.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 13300, LocationType.VICTORY, - logic.terran_rak_shir_requirement - ), + logic.terran_rak_shir_requirement + ), make_location_data(SC2Mission.RAK_SHIR_T.mission_name, "North Slayn Elemental", SC2_RACESWAP_LOC_ID_OFFSET + 13301, LocationType.VANILLA, - logic.terran_rak_shir_requirement - ), + logic.terran_rak_shir_requirement, + hard_rule=logic.terran_any_anti_air, + ), make_location_data(SC2Mission.RAK_SHIR_T.mission_name, "Southwest Slayn Elemental", SC2_RACESWAP_LOC_ID_OFFSET + 13302, LocationType.VANILLA, - logic.terran_rak_shir_requirement - ), + logic.terran_rak_shir_requirement, + hard_rule=logic.terran_any_anti_air, + ), make_location_data(SC2Mission.RAK_SHIR_T.mission_name, "East Slayn Elemental", SC2_RACESWAP_LOC_ID_OFFSET + 13303, LocationType.VANILLA, - logic.terran_rak_shir_requirement - ), + logic.terran_rak_shir_requirement, + hard_rule=logic.terran_any_anti_air, + ), make_location_data(SC2Mission.RAK_SHIR_Z.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 13400, LocationType.VICTORY, - logic.zerg_rak_shir_requirement - ), + logic.zerg_rak_shir_requirement + ), make_location_data(SC2Mission.RAK_SHIR_Z.mission_name, "North Slayn Elemental", SC2_RACESWAP_LOC_ID_OFFSET + 13401, LocationType.VANILLA, - logic.zerg_rak_shir_requirement - ), + logic.zerg_rak_shir_requirement, + hard_rule=logic.zerg_any_anti_air, + ), make_location_data(SC2Mission.RAK_SHIR_Z.mission_name, "Southwest Slayn Elemental", SC2_RACESWAP_LOC_ID_OFFSET + 13402, LocationType.VANILLA, - logic.zerg_rak_shir_requirement - ), + logic.zerg_rak_shir_requirement, + hard_rule=logic.zerg_any_anti_air, + ), make_location_data(SC2Mission.RAK_SHIR_Z.mission_name, "East Slayn Elemental", SC2_RACESWAP_LOC_ID_OFFSET + 13403, LocationType.VANILLA, - logic.zerg_rak_shir_requirement - ), + logic.zerg_rak_shir_requirement, + hard_rule=logic.zerg_any_anti_air, + ), make_location_data(SC2Mission.TEMPLAR_S_CHARGE_T.mission_name, "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 13500, LocationType.VICTORY, - logic.terran_templars_charge_requirement - ), + logic.terran_templars_charge_requirement + ), make_location_data(SC2Mission.TEMPLAR_S_CHARGE_T.mission_name, "Northwest Power Core", SC2_RACESWAP_LOC_ID_OFFSET + 13501, LocationType.EXTRA, logic.terran_templars_charge_requirement ), @@ -5806,7 +5875,7 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: # Generating Beat event and Victory Cache locations if location_data.type == LocationType.VICTORY: beat_events.append( - location_data._replace(name="Beat " + location_data.region, code=None) + location_data._replace(name="Beat " + location_data.region, code=None) # type: ignore ) for v in range(VICTORY_CACHE_SIZE): victory_caches.append( diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py index 7a530ddf14fb..4a5918834035 100644 --- a/worlds/sc2/rules.py +++ b/worlds/sc2/rules.py @@ -5,7 +5,8 @@ from .options import ( get_option_value, RequiredTactics, kerrigan_unit_available, AllInMap, GrantStoryTech, GrantStoryLevels, SpearOfAdunAutonomouslyCastAbilityPresence, - get_enabled_campaigns, MissionOrder, EnableMorphling, get_enabled_races + SpearOfAdunPresence, MissionOrder, EnableMorphling, + get_enabled_campaigns, get_enabled_races, ) from .item.item_tables import ( tvx_defense_ratings, tvz_defense_ratings, kerrigan_actives, tvx_air_defense_ratings, @@ -279,6 +280,37 @@ def welcome_to_the_jungle_p_requirement(self, state: CollectionState) -> bool: ) ) ) + + def terran_any_anti_air(self, state: CollectionState) -> bool: + return ( + state.has_any(( + # Barracks + item_names.MARINE, item_names.WAR_PIGS, item_names.SON_OF_KORHAL, + item_names.DOMINION_TROOPER, + item_names.GHOST, item_names.SPECTRE, item_names.EMPERORS_SHADOW, + # Factory + item_names.GOLIATH, item_names.SPARTAN_COMPANY, item_names.BULWARK_COMPANY, + item_names.CYCLONE, item_names.WIDOW_MINE, + item_names.THOR, item_names.JOTUN, item_names.BLACKHAMMER, + # Ships + item_names.WRAITH, item_names.WINGED_NIGHTMARES, item_names.NIGHT_HAWK, + item_names.VIKING, item_names.HELS_ANGELS, item_names.SKY_FURY, + item_names.LIBERATOR, item_names.MIDNIGHT_RIDERS, item_names.EMPERORS_GUARDIAN, + item_names.VALKYRIE, item_names.BRYNHILDS, + item_names.BATTLECRUISER, item_names.JACKSONS_REVENGE, item_names.PRIDE_OF_AUGUSTRGRAD, + item_names.RAVEN, + # Buildings + item_names.MISSILE_TURRET, + ), self.player) + or state.has_all((item_names.REAPER, item_names.REAPER_JET_PACK_OVERDRIVE), self.player) + or state.has_all((item_names.PLANETARY_FORTRESS, item_names.PLANETARY_FORTRESS_ADVANCED_TARGETING), self.player) + or ( + state.has(item_names.MEDIVAC, self.player) + and state.has_any((item_names.SIEGE_TANK, item_names.SIEGE_BREAKERS, item_names.SHOCK_DIVISION), self.player) + and state.count(item_names.SIEGE_TANK_PROGRESSIVE_TRANSPORT_HOOK, self.player) >= 2 + ) + ) + def terran_basic_anti_air(self, state: CollectionState) -> bool: """ @@ -885,6 +917,29 @@ def zerg_competent_anti_air(self, state: CollectionState) -> bool: or state.has_all({item_names.SCOURGE, item_names.SCOURGE_RESOURCE_EFFICIENCY}, self.player) or (self.advanced_tactics and state.has(item_names.INFESTOR, self.player)) ) + + def zerg_kerrigan_or_any_anti_air(self, state: CollectionState) -> bool: + return self.kerrigan_unit_available or self.zerg_any_anti_air(state) + + def zerg_any_anti_air(self, state: CollectionState) -> bool: + return ( + state.has_any(( + item_names.HYDRALISK, item_names.SWARM_QUEEN, + item_names.BROOD_QUEEN, item_names.MUTALISK, item_names.CORRUPTOR, item_names.SCOURGE, + item_names.INFESTOR, + item_names.INFESTED_MARINE, item_names.INFESTED_LIBERATOR, + item_names.SPORE_CRAWLER, item_names.INFESTED_MISSILE_TURRET, + item_names.INFESTED_BUNKER, + item_names.HUNTER_KILLERS, item_names.THORNSHELL, + ), self.player) + or state.has_all((item_names.SWARM_HOST, item_names.SWARM_HOST_PRESSURIZED_GLANDS), self.player) + or state.has_all((item_names.ABERRATION, item_names.ABERRATION_PROGRESSIVE_BANELING_LAUNCH), self.player) + or state.has_all((item_names.INFESTED_DIAMONDBACK, item_names.INFESTED_DIAMONDBACK_PROGRESSIVE_FUNGAL_SNARE), self.player) + or self.morph_ravager(state) + or self.morph_viper(state) + or self.morph_devourer(state) + or (self.morph_guardian(state) and state.has(item_names.GUARDIAN_PRIMAL_ADAPTATION, self.player)) + ) def zerg_basic_anti_air(self, state: CollectionState) -> bool: return self.zerg_basic_kerriganless_anti_air(state) or self.kerrigan_unit_available @@ -933,6 +988,12 @@ def zerg_infested_tank_with_ammo(self, state: CollectionState) -> bool: ) ) + def morph_ravager(self, state: CollectionState) -> bool: + return ( + (state.has(item_names.ROACH, self.player) or self.morphling_enabled) + and state.has(item_names.ROACH_RAVAGER_ASPECT, self.player) + ) + def morph_brood_lord(self, state: CollectionState) -> bool: return ( (state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player) or self.morphling_enabled) @@ -1331,6 +1392,61 @@ def protoss_defense_rating(self, state: CollectionState, zerg_enemy: bool) -> in def protoss_common_unit(self, state: CollectionState) -> bool: return state.has_any(self.basic_protoss_units, self.player) + + def protoss_any_anti_air_unit_or_soa_any_protoss(self, state: CollectionState) -> bool: + return ( + self.protoss_any_anti_air_unit(state) + or ( + self.spear_of_adun_presence in (SpearOfAdunPresence.option_everywhere, SpearOfAdunPresence.option_protoss) + and self.protoss_any_anti_air_soa(state) + ) + ) + + def protoss_any_anti_air_unit_or_soa(self, state: CollectionState) -> bool: + return self.protoss_any_anti_air_unit(state) or self.protoss_any_anti_air_soa(state) + + def protoss_any_anti_air_soa(self, state: CollectionState) -> bool: + return ( + state.has_any(( + item_names.SOA_ORBITAL_STRIKE, item_names.SOA_SOLAR_LANCE, + item_names.SOA_SOLAR_BOMBARDMENT, item_names.SOA_PURIFIER_BEAM, + item_names.SOA_PYLON_OVERCHARGE, + ), self.player) + or state.has(item_names.SOA_PROGRESSIVE_PROXY_PYLON, self.player, 2) # Reinforcements) + ) + + def protoss_any_anti_air_unit(self, state: CollectionState) -> bool: + return ( + state.has_any(( + # Gateway + item_names.STALKER, item_names.SLAYER, item_names.INSTIGATOR, + item_names.DRAGOON, item_names.ADEPT, + item_names.SENTRY, item_names.ENERGIZER, + item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.ASCENDANT, item_names.DARK_ARCHON, + item_names.DARK_TEMPLAR, # Archon, Dark Archon Meld + # Stargate + item_names.PHOENIX, item_names.MIRAGE, item_names.CORSAIR, + item_names.SCOUT, item_names.ARBITER, + item_names.VOID_RAY, item_names.DESTROYER, item_names.WARP_RAY, + item_names.CARRIER, item_names.SKYLORD, item_names.TEMPEST, + item_names.MOTHERSHIP, + # Buildings + item_names.NEXUS_OVERCHARGE, item_names.PHOTON_CANNON, item_names.KHAYDARIN_MONOLITH, + ), self.player) + or state.has_all((item_names.SUPPLICANT, item_names.SUPPLICANT_ZENITH_PITCH), self.player) + or state.has_all((item_names.WARP_PRISM, item_names.WARP_PRISM_PHASE_BLASTER), self.player) + or state.has_all((item_names.WRATHWALKER, item_names.WRATHWALKER_AERIAL_TRACKING), self.player) + or state.has_all((item_names.DISRUPTOR, item_names.DISRUPTOR_PERFECTED_POWER), self.player) + or state.has_all((item_names.IMMORTAL, item_names.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS), self.player) + or (state.has(item_names.ANNIHILATOR, self.player) + and state.has_any(( + item_names.ANNIHILATOR_AERIAL_TRACKING, + item_names.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS, + ), self.player) + ) + or state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player) + or state.has_all((item_names.TRIREME, item_names.TRIREME_SOLAR_BEAM), self.player) + ) def protoss_basic_anti_air(self, state: CollectionState) -> bool: return ( @@ -2096,6 +2212,13 @@ def amons_fall_requirement(self, state: CollectionState) -> bool: ) else: return state.has(item_names.MUTALISK, self.player) and self.zerg_competent_comp(state) + + def nova_any_nobuild_damage(self, state: CollectionState) -> bool: + return state.has_any(( + item_names.NOVA_C20A_CANISTER_RIFLE, item_names.NOVA_HELLFIRE_SHOTGUN, item_names.NOVA_PLASMA_RIFLE, + item_names.NOVA_MONOMOLECULAR_BLADE, item_names.NOVA_BLAZEFIRE_GUNBLADE, + item_names.NOVA_PULSE_GRENADES, item_names.NOVA_DOMINATION, + ), self.player) def nova_any_weapon(self, state: CollectionState) -> bool: return state.has_any({ @@ -2249,6 +2372,18 @@ def enemy_intelligence_third_stage_requirement(self, state: CollectionState) -> ) ) ) + + def enemy_intelligence_cliff_garrison_and_nova_mobility(self, state: CollectionState) -> bool: + return ( + self.enemy_intelligence_cliff_garrison(state) + and ( + self.nova_any_nobuild_damage(state) + or ( + state.has(item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player, 2) + and state.has_any((item_names.NOVA_FLASHBANG_GRENADES, item_names.NOVA_BLINK), self.player) + ) + ) + ) def trouble_in_paradise_requirement(self, state: CollectionState) -> bool: return ( @@ -2307,6 +2442,15 @@ def enemy_shadow_door_unlocks_tool(self, state: CollectionState) -> bool: return state.has_any({ item_names.NOVA_DOMINATION, item_names.NOVA_BLINK, item_names.NOVA_JUMP_SUIT_MODULE }, self.player) + + def enemy_shadow_nova_damage_and_blazefire_unlock(self, state: CollectionState) -> bool: + return ( + self.nova_any_nobuild_damage(state) + and ( + state.has(item_names.NOVA_BLINK, self.player) + or state.has_all((item_names.NOVA_HOLO_DECOY, item_names.NOVA_DOMINATION), self.player) + ) + ) def enemy_shadow_domination(self, state: CollectionState) -> bool: return ( @@ -2394,6 +2538,7 @@ def __init__(self, world: Optional['SC2World']): self.basic_terran_units = get_basic_units(self.logic_level, SC2Race.TERRAN) self.basic_zerg_units = get_basic_units(self.logic_level, SC2Race.ZERG) self.basic_protoss_units = get_basic_units(self.logic_level, SC2Race.PROTOSS) + self.spear_of_adun_presence = SpearOfAdunPresence.default if world is None else world.options.spear_of_adun_presence.value self.spear_of_adun_autonomously_cast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence") self.enabled_campaigns = get_enabled_campaigns(world) self.mission_order = get_option_value(world, "mission_order") @@ -2428,7 +2573,12 @@ def _has_zerg_units(state: CollectionState) -> bool: def has_protoss_units(player: int, target: int) -> Callable[['CollectionState'], bool]: def _has_protoss_units(state: CollectionState) -> bool: return ( - state.count_from_list_unique(item_groups.protoss_units + item_groups.protoss_buildings, player) >= target + state.count_from_list_unique( + item_groups.protoss_units + + item_groups.protoss_buildings + + [item_names.NEXUS_OVERCHARGE], + player + ) >= target ) return _has_protoss_units From 9be1aa70255817f53e3f3d5c4ed0d3c37b5f9c8f Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 27 Dec 2024 15:54:17 -0800 Subject: [PATCH 7/9] sc2: Updated setup docs with new logic level --- worlds/sc2/docs/setup_en.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md index 5b378873f4a3..0f9220ec4a57 100644 --- a/worlds/sc2/docs/setup_en.md +++ b/worlds/sc2/docs/setup_en.md @@ -61,10 +61,39 @@ If the Progression Balancing of one world is greater than that of others, items obtained early, and vice versa if its value is smaller. However, StarCraft 2 is more permissive regarding the items that can be used to progress, so this option has little influence on progression in a StarCraft 2 world. -StarCraft 2. Since this option increases the time required to generate a MultiWorld, we recommend deactivating it (i.e., setting it to zero) for a StarCraft 2 world. +#### What does Tactics Level do? + +Tactics level allows controlling the difficulty through what items you're likely to get early. +This is independent of game difficulty like causal, normal, hard, or brutal. + +"Standard" and "Advanced" levels are guaranteed to be beatable with the items you are given. +The logic is a little more restrictive than a player's creativity, so an advanced player is likely to have +more items than they need in any situation. These levels are entirely safe to use in a multiworld. + +The "Any Units" level only guarantees that a minimum number of faction-appropriate units or buildings are reachable +early on, but not what those units are. +Before starting a build mission, the generator will guarantee that N units or buildings can be acquired before starting it, +where N is the number of missions the player needs to beat to access the mission, and the units belong to the faction +the player will play in the mission. For a linear order of missions, ordered [zerg, protoss, terran], a protoss unit +is guaranteed to be unlocked in the zerg mission, and 2 terran units are guaranteed to be unlocked +in the preceding 2 missions. This effect maxes out at 5 units guaranteed for each faction. + +It's possible to get stuck on "Any Units" if the units can't attack, +like getting only medics and medivacs for the first 2 units, only getting units that take too long to build +for the missions at hand, or simply not having enough damage output. +Some safeguards exist to make sure terrain traversal, no-builds, and having something that can hit air objectives exists, +meaning cheat codes like `terribleterribledamage` can be used to recover a seemingly stuck world. +This logic option is likely to be beatable without cheats, but not guaranteed to be. +Thus, it is only safe to use in a multiworld if the player is willing to use cheats to get a world unstuck if +the situation calls for it, or uses settings like start inventory or mission exclusions to guarantee beatability. + +The "No Logic" level provides no logical safeguards for beatability. It is only safe to use in a multiworld if the player curates +a start inventory or the organizer is okay with the possibility of the StarCraft 2 world being unbeatable. +Safeguards exist so that other games' items placed in the StarCraft 2 world are reachable under "Advanced" logic rules. + #### How do I specify items in a list, like in excluded items? You can look up the syntax for yaml collections in the @@ -102,8 +131,6 @@ for each game that it currently supports, including StarCraft 2. You can also look up a complete list of the item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page. This page also contains supplementary information of each item. -However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the -former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development. As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over the mission in the 'StarCraft 2 Launcher' tab in the client. From 3ba582b1389206ac4dcc8623e0f18aecc400304f Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 28 Dec 2024 02:25:33 -0800 Subject: [PATCH 8/9] sc2: Updating tactics level doc to include reducing difficulty as a way to recover an any units game --- worlds/sc2/docs/setup_en.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md index 0f9220ec4a57..434f654479d8 100644 --- a/worlds/sc2/docs/setup_en.md +++ b/worlds/sc2/docs/setup_en.md @@ -85,7 +85,8 @@ It's possible to get stuck on "Any Units" if the units can't attack, like getting only medics and medivacs for the first 2 units, only getting units that take too long to build for the missions at hand, or simply not having enough damage output. Some safeguards exist to make sure terrain traversal, no-builds, and having something that can hit air objectives exists, -meaning cheat codes like `terribleterribledamage` can be used to recover a seemingly stuck world. +so a stuck world can be recovered by either setting the difficulty to casual with `/difficulty casual` in the client +or using cheat codes like `terribleterribledamage` in-game. This logic option is likely to be beatable without cheats, but not guaranteed to be. Thus, it is only safe to use in a multiworld if the player is willing to use cheats to get a world unstuck if the situation calls for it, or uses settings like start inventory or mission exclusions to guarantee beatability. From c74e31aa07485c9a1dfc7c21e73a62ae0a3928cb Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 28 Dec 2024 16:17:43 -0800 Subject: [PATCH 9/9] sc2: Updated ValidInventory to allow passing count to has(); added new methods to TestInventory in test_rules --- worlds/sc2/pool_filter.py | 4 ++-- worlds/sc2/rules.py | 2 +- worlds/sc2/test/test_rules.py | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/worlds/sc2/pool_filter.py b/worlds/sc2/pool_filter.py index 4695daa65b63..4ea0f8f04024 100644 --- a/worlds/sc2/pool_filter.py +++ b/worlds/sc2/pool_filter.py @@ -124,8 +124,8 @@ def __init__(self, world: 'SC2World', item_pool: List[StarcraftItem]) -> None: for parent_item in item_parents.child_item_to_parent_items.get(item.name, []): self.item_name_to_child_items.setdefault(parent_item, []).append(item) - def has(self, item: str, player: int) -> bool: - return self.logical_inventory.get(item, 0) > 0 + def has(self, item: str, player: int, count: int = 1) -> bool: + return self.logical_inventory.get(item, 0) >= count def has_any(self, items: Set[str], player: int) -> bool: return any(self.logical_inventory.get(item) for item in items) diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py index 4a5918834035..8c5d1b03fcbd 100644 --- a/worlds/sc2/rules.py +++ b/worlds/sc2/rules.py @@ -1412,7 +1412,7 @@ def protoss_any_anti_air_soa(self, state: CollectionState) -> bool: item_names.SOA_SOLAR_BOMBARDMENT, item_names.SOA_PURIFIER_BEAM, item_names.SOA_PYLON_OVERCHARGE, ), self.player) - or state.has(item_names.SOA_PROGRESSIVE_PROXY_PYLON, self.player, 2) # Reinforcements) + or state.has(item_names.SOA_PROGRESSIVE_PROXY_PYLON, self.player, 2) # Warp-In Reinforcements ) def protoss_any_anti_air_unit(self, state: CollectionState) -> bool: diff --git a/worlds/sc2/test/test_rules.py b/worlds/sc2/test/test_rules.py index a52cc4aad61f..34c00e441d30 100644 --- a/worlds/sc2/test/test_rules.py +++ b/worlds/sc2/test/test_rules.py @@ -24,7 +24,7 @@ def is_item_progression(self, item: str) -> bool: def random_boolean(self): return self.random.choice([True, False]) - def has(self, item: str, player: int): + def has(self, item: str, player: int, count: int = 1): if not self.is_item_progression(item): raise AssertionError("Logic item {} is not a progression item".format(item)) return self.random_boolean() @@ -56,6 +56,9 @@ def count(self, item: str, player: int) -> int: def count_from_list(self, items: Iterable[str], player: int) -> int: return sum(self.count(item_name, player) for item_name in items) + def count_from_list_unique(self, items: Iterable[str], player: int) -> int: + return sum(self.count(item_name, player) for item_name in items) + class TestWorld: """