diff --git a/worlds/sc2/item/item_tables.py b/worlds/sc2/item/item_tables.py index 7fb198ea..d63b0048 100644 --- a/worlds/sc2/item/item_tables.py +++ b/worlds/sc2/item/item_tables.py @@ -1860,7 +1860,7 @@ item_table = { item_names.DARK_TEMPLAR_ARCHON_MERGE: ItemData(417 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 27, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.DARK_TEMPLAR), item_names.ASCENDANT_ARCHON_MERGE: ItemData(418 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 28, SC2Race.PROTOSS, classification=ItemClassification.progression_skip_balancing, parent=item_names.ASCENDANT), item_names.SCOUT_SUPPLY_EFFICIENCY: ItemData(419 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 29, SC2Race.PROTOSS, parent=item_names.SCOUT), - item_names.REAVER_BARGAIN_BIN_PRICES: ItemData(420 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_5, 0, SC2Race.PROTOSS, parent=item_names.SCOUT), + item_names.REAVER_BARGAIN_BIN_PRICES: ItemData(420 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_5, 0, SC2Race.PROTOSS, parent=item_names.REAVER), # War Council diff --git a/worlds/sc2/locations.py b/worlds/sc2/locations.py index 6b505d9c..34245d86 100644 --- a/worlds/sc2/locations.py +++ b/worlds/sc2/locations.py @@ -2535,6 +2535,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: 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.SHOOT_THE_MESSENGER.mission_name, @@ -2569,6 +2570,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: 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.SHOOT_THE_MESSENGER.mission_name, @@ -2610,6 +2612,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2HOTS_LOC_ID_OFFSET + 510, LocationType.CHALLENGE, logic.zerg_competent_comp_competent_aa, + hard_rule=logic.zerg_any_anti_air, flags=LocationFlag.BASEBUST, ), make_location_data( @@ -2618,6 +2621,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2HOTS_LOC_ID_OFFSET + 511, LocationType.CHALLENGE, logic.zerg_competent_comp_competent_aa, + hard_rule=logic.zerg_any_anti_air, flags=LocationFlag.BASEBUST, ), make_location_data( @@ -2626,6 +2630,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2HOTS_LOC_ID_OFFSET + 512, LocationType.CHALLENGE, logic.zerg_competent_comp_competent_aa, + hard_rule=logic.zerg_any_anti_air, flags=LocationFlag.BASEBUST, ), make_location_data( @@ -10042,6 +10047,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: logic.terran_common_unit(state) and logic.terran_competent_anti_air(state) ), + hard_rule=logic.terran_any_anti_air, ), make_location_data( SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, @@ -10079,6 +10085,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: logic.terran_common_unit(state) and logic.terran_competent_anti_air(state) ), + hard_rule=logic.terran_any_anti_air, ), make_location_data( SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, @@ -10122,7 +10129,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2_RACESWAP_LOC_ID_OFFSET + 6710, LocationType.CHALLENGE, lambda state: logic.terran_beats_protoss_deathball(state) - and logic.terran_common_unit(state), + and logic.terran_common_unit(state), flags=LocationFlag.BASEBUST, ), make_location_data( @@ -10131,8 +10138,9 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2_RACESWAP_LOC_ID_OFFSET + 6711, LocationType.CHALLENGE, lambda state: logic.terran_beats_protoss_deathball(state) - and logic.terran_competent_ground_to_air(state) - and logic.terran_common_unit(state), + and logic.terran_competent_ground_to_air(state) + and logic.terran_common_unit(state), + hard_rule=logic.terran_any_anti_air, flags=LocationFlag.BASEBUST, ), make_location_data( @@ -10141,8 +10149,9 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2_RACESWAP_LOC_ID_OFFSET + 6712, LocationType.CHALLENGE, lambda state: logic.terran_beats_protoss_deathball(state) - and logic.terran_competent_ground_to_air(state) - and logic.terran_common_unit(state), + and logic.terran_competent_ground_to_air(state) + and logic.terran_common_unit(state), + hard_rule=logic.terran_any_anti_air, flags=LocationFlag.BASEBUST, ), make_location_data( @@ -10154,6 +10163,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: logic.protoss_common_unit(state) and logic.protoss_anti_armor_anti_air(state) ), + hard_rule=logic.protoss_any_anti_air_unit, ), make_location_data( SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, @@ -10191,6 +10201,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: logic.protoss_common_unit(state) and logic.protoss_anti_armor_anti_air(state) ), + hard_rule=logic.protoss_any_anti_air_unit, ), make_location_data( SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, @@ -10238,6 +10249,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2_RACESWAP_LOC_ID_OFFSET + 6810, LocationType.CHALLENGE, logic.protoss_competent_comp, + hard_rule=logic.protoss_any_anti_air_unit, flags=LocationFlag.BASEBUST, ), make_location_data( @@ -10246,6 +10258,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2_RACESWAP_LOC_ID_OFFSET + 6811, LocationType.CHALLENGE, logic.protoss_competent_comp, + hard_rule=logic.protoss_any_anti_air_unit, flags=LocationFlag.BASEBUST, ), make_location_data( @@ -10254,6 +10267,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2_RACESWAP_LOC_ID_OFFSET + 6812, LocationType.CHALLENGE, logic.protoss_competent_comp, + hard_rule=logic.protoss_any_anti_air_unit, flags=LocationFlag.BASEBUST, ), make_location_data( diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py index 2a03d65d..2298c2ce 100644 --- a/worlds/sc2/rules.py +++ b/worlds/sc2/rules.py @@ -3367,65 +3367,74 @@ class SC2Logic: def has_terran_units(self, 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, self.player) >= target) and ( - # Anything that can hit buildings - state.has_any(( - # Infantry - item_names.MARINE, - item_names.FIREBAT, - item_names.MARAUDER, - item_names.REAPER, - item_names.HERC, - item_names.DOMINION_TROOPER, - item_names.GHOST, - item_names.SPECTRE, - # Vehicles - item_names.HELLION, - item_names.VULTURE, - item_names.SIEGE_TANK, - item_names.WARHOUND, - item_names.GOLIATH, - item_names.DIAMONDBACK, - item_names.THOR, - item_names.PREDATOR, - item_names.CYCLONE, - # Ships - item_names.WRAITH, - item_names.VIKING, - item_names.BANSHEE, - item_names.RAVEN, - item_names.BATTLECRUISER, - # RG - item_names.SON_OF_KORHAL, - item_names.AEGIS_GUARD, - item_names.EMPERORS_SHADOW, - item_names.BULWARK_COMPANY, - item_names.SHOCK_DIVISION, - item_names.BLACKHAMMER, - item_names.SKY_FURY, - item_names.NIGHT_WOLF, - item_names.NIGHT_HAWK, - item_names.PRIDE_OF_AUGUSTRGRAD, - ), self.player) - or state.has_all((item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY), self.player) - or state.has_all((item_names.EMPERORS_GUARDIAN, item_names.LIBERATOR_RAID_ARTILLERY), self.player) - or state.has_all((item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES), self.player) - or state.has_all((item_names.WIDOW_MINE, item_names.WIDOW_MINE_DEMOLITION_PAYLOAD), self.player) - or ( + return ( + state.count_from_list_unique( + item_groups.terran_units + item_groups.terran_buildings, self.player + ) >= target + and ( + target < 5 + or self.terran_any_anti_air(state) + ) + and ( + # Anything that can hit buildings state.has_any(( - # Mercs with shortest initial cooldown (300s) - item_names.WAR_PIGS, - item_names.DEATH_HEADS, - item_names.HELS_ANGELS, - item_names.WINGED_NIGHTMARES, + # Infantry + item_names.MARINE, + item_names.FIREBAT, + item_names.MARAUDER, + item_names.REAPER, + item_names.HERC, + item_names.DOMINION_TROOPER, + item_names.GHOST, + item_names.SPECTRE, + # Vehicles + item_names.HELLION, + item_names.VULTURE, + item_names.SIEGE_TANK, + item_names.WARHOUND, + item_names.GOLIATH, + item_names.DIAMONDBACK, + item_names.THOR, + item_names.PREDATOR, + item_names.CYCLONE, + # Ships + item_names.WRAITH, + item_names.VIKING, + item_names.BANSHEE, + item_names.RAVEN, + item_names.BATTLECRUISER, + # RG + item_names.SON_OF_KORHAL, + item_names.AEGIS_GUARD, + item_names.EMPERORS_SHADOW, + item_names.BULWARK_COMPANY, + item_names.SHOCK_DIVISION, + item_names.BLACKHAMMER, + item_names.SKY_FURY, + item_names.NIGHT_WOLF, + item_names.NIGHT_HAWK, + item_names.PRIDE_OF_AUGUSTRGRAD, ), self.player) - # + 2 upgrades that allow getting faster/earlier mercs - and state.count_from_list(( - item_names.RAPID_REINFORCEMENT, - item_names.PROGRESSIVE_FAST_DELIVERY, - item_names.ROGUE_FORCES, - # item_names.SIGNAL_BEACON, # Probably doesn't help too much on the first unit - ), self.player) >= 2 + or state.has_all((item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY), self.player) + or state.has_all((item_names.EMPERORS_GUARDIAN, item_names.LIBERATOR_RAID_ARTILLERY), self.player) + or state.has_all((item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES), self.player) + or state.has_all((item_names.WIDOW_MINE, item_names.WIDOW_MINE_DEMOLITION_PAYLOAD), self.player) + or ( + state.has_any(( + # Mercs with shortest initial cooldown (300s) + item_names.WAR_PIGS, + item_names.DEATH_HEADS, + item_names.HELS_ANGELS, + item_names.WINGED_NIGHTMARES, + ), self.player) + # + 2 upgrades that allow getting faster/earlier mercs + and state.count_from_list(( + item_names.RAPID_REINFORCEMENT, + item_names.PROGRESSIVE_FAST_DELIVERY, + item_names.ROGUE_FORCES, + # item_names.SIGNAL_BEACON, # Probably doesn't help too much on the first unit + ), self.player) >= 2 + ) ) ) @@ -3451,6 +3460,10 @@ class SC2Logic: ) return ( num_units >= target + and ( + target < 5 + or self.zerg_any_anti_air(state) + ) and ( # Anything that can hit buildings state.has_any(( @@ -3468,11 +3481,6 @@ class SC2Logic: item_names.INFESTED_DIAMONDBACK, item_names.INFESTED_SIEGE_TANK, item_names.INFESTED_BANSHEE, - # Mercs with <= 300s first drop time - item_names.DEVOURING_ONES, - item_names.HUNTER_KILLERS, - item_names.CAUSTIC_HORRORS, - item_names.HUNTERLING, ), self.player) or state.has_all((item_names.INFESTOR, item_names.INFESTOR_INFESTED_TERRAN), self.player) or self.morph_baneling(state) @@ -3512,6 +3520,9 @@ class SC2Logic: return ( state.count_from_list_unique(item_groups.protoss_units + item_groups.protoss_buildings + [item_names.NEXUS_OVERCHARGE], self.player) >= target + ) and ( + target < 5 + or self.protoss_any_anti_air_unit(state) ) and ( # Anything that can hit buildings state.has_any((