SC2: Logic bugfixes (#5461)

* sc2: Fixing always-true rules in locations.py; fixed two over-constrained rules that put vanilla out-of-logic

* sc2: Minor min2() optimization in rules.py

* sc2: Fixing a Shatter the Sky logic bug where w/a upgrades were checked too many times and for the wrong units
This commit is contained in:
Phaneros
2025-09-21 09:54:22 -07:00
committed by GitHub
parent 3af1e92813
commit 7badc3e745
2 changed files with 88 additions and 90 deletions

View File

@@ -2150,8 +2150,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"Victory",
SC2WOL_LOC_ID_OFFSET + 2800,
LocationType.VICTORY,
lambda state: logic.terran_competent_comp(state)
and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2,
lambda state: logic.terran_competent_comp(state, 2),
),
make_location_data(
SC2Mission.SHATTER_THE_SKY.mission_name,
@@ -2172,24 +2171,21 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"Southeast Coolant Tower",
SC2WOL_LOC_ID_OFFSET + 2803,
LocationType.VANILLA,
lambda state: logic.terran_competent_comp(state)
and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2,
lambda state: logic.terran_competent_comp(state, 2),
),
make_location_data(
SC2Mission.SHATTER_THE_SKY.mission_name,
"Southwest Coolant Tower",
SC2WOL_LOC_ID_OFFSET + 2804,
LocationType.VANILLA,
lambda state: logic.terran_competent_comp(state)
and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2,
lambda state: logic.terran_competent_comp(state, 2),
),
make_location_data(
SC2Mission.SHATTER_THE_SKY.mission_name,
"Leviathan",
SC2WOL_LOC_ID_OFFSET + 2805,
LocationType.VANILLA,
lambda state: logic.terran_competent_comp(state)
and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2,
lambda state: logic.terran_competent_comp(state, 2),
hard_rule=logic.terran_any_anti_air,
),
make_location_data(
@@ -2262,7 +2258,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
SC2HOTS_LOC_ID_OFFSET + 100,
LocationType.VICTORY,
lambda state: (
logic.zerg_common_unit
logic.zerg_common_unit(state)
or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player)
),
),
@@ -2279,7 +2275,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
LocationType.VANILLA,
lambda state: adv_tactics
or (
logic.zerg_common_unit
logic.zerg_common_unit(state)
or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player)
),
),
@@ -2290,7 +2286,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
LocationType.VANILLA,
lambda state: adv_tactics
or (
logic.zerg_common_unit
logic.zerg_common_unit(state)
or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player)
),
),
@@ -2301,7 +2297,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
LocationType.VANILLA,
lambda state: adv_tactics
or (
logic.zerg_common_unit
logic.zerg_common_unit(state)
or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player)
),
),
@@ -2324,7 +2320,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
LocationType.EXTRA,
lambda state: adv_tactics
or (
logic.zerg_common_unit
logic.zerg_common_unit(state)
or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player)
),
),
@@ -2334,7 +2330,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
SC2HOTS_LOC_ID_OFFSET + 108,
LocationType.CHALLENGE,
lambda state: (
logic.zerg_common_unit
logic.zerg_common_unit(state)
or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player)
),
flags=LocationFlag.SPEEDRUN,
@@ -3862,15 +3858,11 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
SC2LOTV_LOC_ID_OFFSET + 300,
LocationType.VICTORY,
lambda state: adv_tactics
or state.count_from_list(
(
item_names.STALKER_PHASE_REACTOR,
item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES,
item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION,
),
player,
)
>= 2,
or state.has_any((
item_names.STALKER_PHASE_REACTOR,
item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES,
item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION,
), player),
),
make_location_data(
SC2Mission.EVIL_AWOKEN.mission_name,
@@ -4582,7 +4574,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
lambda state: (
logic.protoss_deathball(state)
and logic.protoss_power_rating(state) >= 6
and (adv_tactics or logic.protoss_fleet(state))
and (adv_tactics or logic.protoss_unsealing_the_past_ledge_requirement(state))
),
),
make_location_data(
@@ -4593,7 +4585,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
lambda state: (
logic.protoss_deathball(state)
and logic.protoss_power_rating(state) >= 6
and (adv_tactics or logic.protoss_fleet(state))
and (adv_tactics or logic.protoss_unsealing_the_past_ledge_requirement(state))
),
),
make_location_data(
@@ -7256,7 +7248,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
SC2_RACESWAP_LOC_ID_OFFSET + 1809,
LocationType.MASTERY,
lambda state: (
logic.protoss_anti_armor_anti_air
logic.protoss_anti_armor_anti_air(state)
and logic.protoss_defense_rating(state, False) >= 6
and logic.protoss_common_unit(state)
and logic.protoss_deathball(state)
@@ -9087,7 +9079,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"Close Obelisk",
SC2_RACESWAP_LOC_ID_OFFSET + 4801,
LocationType.VANILLA,
lambda state: adv_tactics or logic.zerg_common_unit,
lambda state: adv_tactics or logic.zerg_common_unit(state),
),
make_location_data(
SC2Mission.ECHOES_OF_THE_FUTURE_Z.mission_name,
@@ -11841,7 +11833,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"Victory",
SC2_RACESWAP_LOC_ID_OFFSET + 9600,
LocationType.VICTORY,
lambda state: logic.protoss_deathball
lambda state: logic.protoss_deathball(state)
or (adv_tactics and logic.protoss_competent_comp(state)),
),
make_location_data(
@@ -11876,7 +11868,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"Main Path Command Center",
SC2_RACESWAP_LOC_ID_OFFSET + 9605,
LocationType.EXTRA,
lambda state: logic.protoss_deathball
lambda state: logic.protoss_deathball(state)
or (adv_tactics and logic.protoss_competent_comp(state)),
),
make_location_data(
@@ -12026,7 +12018,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"Victory",
SC2_RACESWAP_LOC_ID_OFFSET + 10000,
LocationType.VICTORY,
lambda state: logic.zerg_competent_comp
lambda state: logic.zerg_competent_comp(state)
and logic.zerg_moderate_anti_air(state),
),
make_location_data(
@@ -12034,7 +12026,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"First Prisoner Group",
SC2_RACESWAP_LOC_ID_OFFSET + 10001,
LocationType.VANILLA,
lambda state: logic.zerg_competent_comp
lambda state: logic.zerg_competent_comp(state)
and logic.zerg_moderate_anti_air(state),
),
make_location_data(
@@ -12042,7 +12034,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"Second Prisoner Group",
SC2_RACESWAP_LOC_ID_OFFSET + 10002,
LocationType.VANILLA,
lambda state: logic.zerg_competent_comp
lambda state: logic.zerg_competent_comp(state)
and logic.zerg_moderate_anti_air(state),
),
make_location_data(
@@ -12050,7 +12042,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"First Pylon",
SC2_RACESWAP_LOC_ID_OFFSET + 10003,
LocationType.VANILLA,
lambda state: logic.zerg_competent_comp
lambda state: logic.zerg_competent_comp(state)
and logic.zerg_moderate_anti_air(state),
),
make_location_data(
@@ -12058,7 +12050,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"Second Pylon",
SC2_RACESWAP_LOC_ID_OFFSET + 10004,
LocationType.VANILLA,
lambda state: logic.zerg_competent_comp
lambda state: logic.zerg_competent_comp(state)
and logic.zerg_moderate_anti_air(state),
),
make_location_data(
@@ -12661,7 +12653,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
SC2_RACESWAP_LOC_ID_OFFSET + 11406,
LocationType.CHALLENGE,
lambda state: (
logic.zerg_brothers_in_arms_requirement
logic.zerg_brothers_in_arms_requirement(state)
and logic.zerg_base_buster(state)
and logic.zerg_power_rating(state) >= 8
),

View File

@@ -47,8 +47,15 @@ if TYPE_CHECKING:
from . import SC2World
def min2(a: int, b: int) -> int:
"""`min()` that only takes two values; faster than baseline int by about 2x"""
if a <= b:
return a
return b
class SC2Logic:
def __init__(self, world: Optional["SC2World"]):
def __init__(self, world: Optional["SC2World"]) -> None:
# Note: Don't store a reference to the world so we can cache this object on the world object
self.player = -1 if world is None else world.player
self.logic_level: int = world.options.required_tactics.value if world else RequiredTactics.default
@@ -109,7 +116,7 @@ class SC2Logic:
# has_group with count = 0 is always true for item placement and always false for SC2 item filtering
return state.has_group("Missions", self.player, 0)
def get_very_hard_required_upgrade_level(self):
def get_very_hard_required_upgrade_level(self) -> bool:
return 2 if self.advanced_tactics else 3
def weapon_armor_upgrade_count(self, upgrade_item: str, state: CollectionState) -> int:
@@ -133,7 +140,7 @@ class SC2Logic:
count += 1
return count
def soa_power_rating(self, state: CollectionState):
def soa_power_rating(self, state: CollectionState) -> bool:
power_rating = 0
# Spear of Adun Ultimates (Strongest)
for item, rating in soa_ultimate_ratings.items():
@@ -203,7 +210,7 @@ class SC2Logic:
def terran_common_unit(self, state: CollectionState) -> bool:
return state.has_any(self.basic_terran_units, self.player)
def terran_early_tech(self, state: CollectionState):
def terran_early_tech(self, state: CollectionState) -> bool:
"""
Basic combat unit that can be deployed quickly from mission start
:param state
@@ -447,7 +454,7 @@ class SC2Logic:
defense_score += 2
return defense_score
def terran_competent_comp(self, state: CollectionState) -> bool:
def terran_competent_comp(self, state: CollectionState, upgrade_level: int = 1) -> bool:
# All competent comps require anti-air
if not self.terran_competent_anti_air(state):
return False
@@ -455,12 +462,12 @@ class SC2Logic:
infantry_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, state)
infantry_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, state)
infantry = state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER, item_names.MARAUDER}, self.player)
if infantry_weapons >= 2 and infantry_armor >= 1 and infantry and self.terran_bio_heal(state):
if infantry_weapons >= upgrade_level + 1 and infantry_armor >= upgrade_level and infantry and self.terran_bio_heal(state):
return True
# Mass Air-To-Ground
ship_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, state)
ship_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, state)
if ship_weapons >= 1 and ship_armor >= 1:
if ship_weapons >= upgrade_level and ship_armor >= upgrade_level:
air = (
state.has_any({item_names.BANSHEE, item_names.BATTLECRUISER}, self.player)
or state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY}, self.player)
@@ -473,7 +480,7 @@ class SC2Logic:
# Strong Mech
vehicle_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, state)
vehicle_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, state)
if vehicle_weapons >= 1 and vehicle_armor >= 1:
if vehicle_weapons >= upgrade_level and vehicle_armor >= upgrade_level:
strong_vehicle = state.has_any({item_names.THOR, item_names.SIEGE_TANK}, self.player)
light_frontline = state.has_any(
{item_names.MARINE, item_names.DOMINION_TROOPER, item_names.HELLION, item_names.VULTURE}, self.player
@@ -762,27 +769,27 @@ class SC2Logic:
def zerg_army_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int:
count: int = WEAPON_ARMOR_UPGRADE_MAX_LEVEL
if self.has_zerg_melee_unit:
count = min(count, self.zerg_melee_weapon_armor_upgrade_min_level(state))
count = min2(count, self.zerg_melee_weapon_armor_upgrade_min_level(state))
if self.has_zerg_ranged_unit:
count = min(count, self.zerg_ranged_weapon_armor_upgrade_min_level(state))
count = min2(count, self.zerg_ranged_weapon_armor_upgrade_min_level(state))
if self.has_zerg_air_unit:
count = min(count, self.zerg_flyer_weapon_armor_upgrade_min_level(state))
count = min2(count, self.zerg_flyer_weapon_armor_upgrade_min_level(state))
return count
def zerg_melee_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int:
return min(
return min2(
self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_MELEE_ATTACK, state),
self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE, state),
)
def zerg_ranged_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int:
return min(
return min2(
self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK, state),
self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE, state),
)
def zerg_flyer_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int:
return min(
return min2(
self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_FLYER_ATTACK, state),
self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_FLYER_CARAPACE, state),
)
@@ -1082,20 +1089,17 @@ class SC2Logic:
)
)
def zergling_hydra_roach_start(self, state: CollectionState):
def zergling_hydra_roach_start(self, state: CollectionState) -> bool:
"""
Created mainly for engine of destruction start, but works for other missions with no-build starts.
"""
return state.has_any(
{
return state.has_any((
item_names.ZERGLING_ADRENAL_OVERLOAD,
item_names.HYDRALISK_FRENZY,
item_names.ROACH_HYDRIODIC_BILE,
item_names.ZERGLING_RAPTOR_STRAIN,
item_names.ROACH_CORPSER_STRAIN,
},
self.player,
)
), self.player)
def kerrigan_levels(self, state: CollectionState, target: int, story_levels_available=True) -> bool:
if (story_levels_available and self.story_levels_granted) or not self.kerrigan_unit_available:
@@ -1111,7 +1115,7 @@ class SC2Logic:
# Levels from missions beaten
levels = self.kerrigan_levels_per_mission_completed * state.count_group("Missions", self.player)
if self.kerrigan_levels_per_mission_completed_cap != -1:
levels = min(levels, self.kerrigan_levels_per_mission_completed_cap)
levels = min2(levels, self.kerrigan_levels_per_mission_completed_cap)
# Levels from items
for kerrigan_level_item in kerrigan_levels:
level_amount = get_full_item_list()[kerrigan_level_item].number
@@ -1119,7 +1123,7 @@ class SC2Logic:
levels += item_count * level_amount
# Total level cap
if self.kerrigan_total_level_cap != -1:
levels = min(levels, self.kerrigan_total_level_cap)
levels = min2(levels, self.kerrigan_total_level_cap)
return levels >= target
@@ -1625,20 +1629,17 @@ class SC2Logic:
and state.has_any((item_names.SUPPLICANT, item_names.SHIELD_BATTERY), self.player)
)
def zealot_sentry_slayer_start(self, state: CollectionState):
def zealot_sentry_slayer_start(self, state: CollectionState) -> bool:
"""
Created mainly for engine of destruction start, but works for other missions with no-build starts.
"""
return state.has_any(
{
return state.has_any((
item_names.ZEALOT_WHIRLWIND,
item_names.SENTRY_DOUBLE_SHIELD_RECHARGE,
item_names.SLAYER_PHASE_BLINK,
item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES,
item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION,
},
self.player,
)
), self.player)
# Mission-specific rules
def ghost_of_a_chance_requirement(self, state: CollectionState) -> bool:
@@ -2012,7 +2013,7 @@ class SC2Logic:
and (self.advanced_tactics or state.has(item_names.YGGDRASIL, self.player))
)
def protoss_supernova_requirement(self, state: CollectionState):
def protoss_supernova_requirement(self, state: CollectionState) -> bool:
return (
(
state.count(item_names.PROGRESSIVE_WARP_RELOCATE, self.player) >= 2
@@ -2133,7 +2134,7 @@ class SC2Logic:
and self.protoss_fleet(state)
)
def terran_engine_of_destruction_requirement(self, state: CollectionState) -> int:
def terran_engine_of_destruction_requirement(self, state: CollectionState) -> bool:
power_rating = self.terran_power_rating(state)
if power_rating < 3 or not self.marine_medic_upgrade(state) or not self.terran_common_unit(state):
return False
@@ -2146,7 +2147,7 @@ class SC2Logic:
and state.has_any((item_names.BANSHEE, item_names.LIBERATOR), self.player)
)
def zerg_engine_of_destruction_requirement(self, state: CollectionState) -> int:
def zerg_engine_of_destruction_requirement(self, state: CollectionState) -> bool:
power_rating = self.zerg_power_rating(state)
if (
power_rating < 3
@@ -2161,21 +2162,21 @@ class SC2Logic:
else:
return self.zerg_base_buster(state)
def protoss_engine_of_destruction_requirement(self, state: CollectionState):
def protoss_engine_of_destruction_requirement(self, state: CollectionState) -> bool:
return (
self.zealot_sentry_slayer_start(state)
and self.protoss_repair_odin(state)
and (self.protoss_deathball(state) or self.protoss_fleet(state))
)
def zerg_repair_odin(self, state: CollectionState):
def zerg_repair_odin(self, state: CollectionState) -> bool:
return (
self.zerg_has_infested_scv(state)
or state.has_all({item_names.SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION, item_names.SWARM_QUEEN}, self.player)
or (self.advanced_tactics and state.has(item_names.SWARM_QUEEN, self.player))
)
def protoss_repair_odin(self, state: CollectionState):
def protoss_repair_odin(self, state: CollectionState) -> bool:
return (
state.has(item_names.SENTRY, self.player)
or state.has_all((item_names.CARRIER, item_names.CARRIER_REPAIR_DRONES), self.player)
@@ -2196,7 +2197,7 @@ class SC2Logic:
def protoss_in_utter_darkness_requirement(self, state: CollectionState) -> bool:
return self.protoss_competent_comp(state) and self.protoss_defense_rating(state, True) >= 4
def terran_all_in_requirement(self, state: CollectionState):
def terran_all_in_requirement(self, state: CollectionState) -> bool:
"""
All-in
"""
@@ -2228,7 +2229,7 @@ class SC2Logic:
and state.has_any({item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER, item_names.MISSILE_TURRET}, self.player)
)
def zerg_all_in_requirement(self, state: CollectionState):
def zerg_all_in_requirement(self, state: CollectionState) -> bool:
"""
All-in (Zerg)
"""
@@ -2264,7 +2265,7 @@ class SC2Logic:
and state.has_any({item_names.SPORE_CRAWLER, item_names.INFESTED_MISSILE_TURRET}, self.player)
)
def protoss_all_in_requirement(self, state: CollectionState):
def protoss_all_in_requirement(self, state: CollectionState) -> bool:
"""
All-in (Protoss)
"""
@@ -2436,21 +2437,19 @@ class SC2Logic:
def protoss_can_attack_behind_chasm(self, state: CollectionState) -> bool:
return (
state.has_any(
{
item_names.SCOUT,
item_names.TEMPEST,
item_names.CARRIER,
item_names.SKYLORD,
item_names.TRIREME,
item_names.VOID_RAY,
item_names.DESTROYER,
item_names.PULSAR,
item_names.DAWNBRINGER,
item_names.MOTHERSHIP,
},
self.player,
)
state.has_any((
item_names.SCOUT,
item_names.SKIRMISHER,
item_names.TEMPEST,
item_names.CARRIER,
item_names.SKYLORD,
item_names.TRIREME,
item_names.VOID_RAY,
item_names.DESTROYER,
item_names.PULSAR,
item_names.DAWNBRINGER,
item_names.MOTHERSHIP,
), self.player)
or self.protoss_has_blink(state)
or (
state.has(item_names.WARP_PRISM, self.player)
@@ -2697,6 +2696,12 @@ class SC2Logic:
and (self.take_over_ai_allies or (self.zerg_competent_comp(state) and self.zerg_big_monsters(state)))
and self.zerg_power_rating(state) >= 6
)
def protoss_unsealing_the_past_ledge_requirement(self, state: CollectionState) -> bool:
return (
state.has_any((item_names.COLOSSUS, item_names.WRATHWALKER), self.player)
or self.protoss_can_attack_behind_chasm(state)
)
def terran_unsealing_the_past_requirement(self, state: CollectionState) -> bool:
return (
@@ -2704,7 +2709,7 @@ class SC2Logic:
and self.terran_competent_comp(state)
and self.terran_power_rating(state) >= 6
and (
state.has_all({item_names.SIEGE_TANK, item_names.SIEGE_TANK_JUMP_JETS}, self.player)
state.has_all((item_names.SIEGE_TANK, item_names.SIEGE_TANK_JUMP_JETS), self.player)
or state.has_all(
{item_names.BATTLECRUISER, item_names.BATTLECRUISER_ATX_LASER_BATTERY, item_names.BATTLECRUISER_MOIRAI_IMPULSE_DRIVE}, self.player
)
@@ -3132,8 +3137,9 @@ class SC2Logic:
and (self.terran_cliffjumper(state) or state.has(item_names.BANSHEE, self.player))
and self.nova_splash(state)
and self.terran_defense_rating(state, True, False) >= 3
and self.advanced_tactics
or state.has(item_names.NOVA_JUMP_SUIT_MODULE, self.player)
and (self.advanced_tactics
or state.has(item_names.NOVA_JUMP_SUIT_MODULE, self.player)
)
)
def enemy_intelligence_garrisonable_unit(self, state: CollectionState) -> bool: