From 897d5ab0893c685ef7773eb98eb5da638e090875 Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Tue, 30 Sep 2025 18:35:26 +0200 Subject: [PATCH] SC2: Fix Conviction logic for Grant Story Tech (#5419) * Fix Conviction logic for Grant Story Tech - Kinetic Blast and Crushing Grip is available for the mission if story tech is granted * Review updates --- worlds/sc2/locations.py | 15 +++++------ worlds/sc2/rules.py | 42 +++++++++++++++++------------- worlds/sc2/test/test_generation.py | 1 + worlds/sc2/test/test_usecases.py | 40 +++++++++++++++++++++++++++- 4 files changed, 70 insertions(+), 28 deletions(-) diff --git a/worlds/sc2/locations.py b/worlds/sc2/locations.py index 203d4d26..6b505d9c 100644 --- a/worlds/sc2/locations.py +++ b/worlds/sc2/locations.py @@ -2341,8 +2341,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2HOTS_LOC_ID_OFFSET + 200, LocationType.VICTORY, lambda state: logic.basic_kerrigan(state) - or kerriganless - or logic.grant_story_tech == GrantStoryTech.option_grant, + or kerriganless, hard_rule=logic.zerg_any_units_back_in_the_saddle_requirement, ), make_location_data( @@ -2351,8 +2350,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2HOTS_LOC_ID_OFFSET + 201, LocationType.EXTRA, lambda state: logic.basic_kerrigan(state) - or kerriganless - or logic.grant_story_tech == GrantStoryTech.option_grant, + or kerriganless, hard_rule=logic.zerg_any_units_back_in_the_saddle_requirement, ), make_location_data( @@ -2379,8 +2377,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2HOTS_LOC_ID_OFFSET + 205, LocationType.EXTRA, lambda state: logic.basic_kerrigan(state) - or kerriganless - or logic.grant_story_tech == GrantStoryTech.option_grant, + or kerriganless, hard_rule=logic.zerg_any_units_back_in_the_saddle_requirement, ), make_location_data( @@ -2446,7 +2443,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: lambda state: ( logic.zerg_competent_comp(state) and logic.zerg_competent_anti_air(state) - and (logic.basic_kerrigan(state) or kerriganless) + and (logic.basic_kerrigan(state, False) or kerriganless) and logic.zerg_defense_rating(state, False, False) >= 3 and logic.zerg_power_rating(state) >= 5 ), @@ -3530,7 +3527,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: kerriganless or ( logic.two_kerrigan_actives(state) - and (logic.basic_kerrigan(state) or logic.grant_story_tech == GrantStoryTech.option_grant) + and logic.basic_kerrigan(state) and logic.kerrigan_levels(state, 25) ) ), @@ -3554,7 +3551,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: kerriganless or ( logic.two_kerrigan_actives(state) - and (logic.basic_kerrigan(state) or logic.grant_story_tech == GrantStoryTech.option_grant) + and logic.basic_kerrigan(state) and logic.kerrigan_levels(state, 25) ) ), diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py index e6068ab2..2a03d65d 100644 --- a/worlds/sc2/rules.py +++ b/worlds/sc2/rules.py @@ -1127,8 +1127,10 @@ class SC2Logic: return levels >= target - def basic_kerrigan(self, state: CollectionState) -> bool: - # One active ability that can be used to defeat enemies directly on Standard + def basic_kerrigan(self, state: CollectionState, story_tech_available=True) -> bool: + if story_tech_available and self.grant_story_tech == GrantStoryTech.option_grant: + return True + # One active ability that can be used to defeat enemies directly if not state.has_any( ( item_names.KERRIGAN_LEAPING_STRIKE, @@ -1149,7 +1151,9 @@ class SC2Logic: return True return False - def two_kerrigan_actives(self, state: CollectionState) -> bool: + def two_kerrigan_actives(self, state: CollectionState, story_tech_available=True) -> bool: + if story_tech_available and self.grant_story_tech == GrantStoryTech.option_grant: + return True count = 0 for i in range(7): if state.has_any(kerrigan_logic_active_abilities, self.player): @@ -2396,7 +2400,7 @@ class SC2Logic: return ( self.zerg_competent_comp(state) and (self.zerg_competent_anti_air(state) or self.advanced_tactics and self.zerg_moderate_anti_air(state)) - and (self.basic_kerrigan(state) or self.zerg_power_rating(state) >= 4) + and (self.basic_kerrigan(state, False) or self.zerg_power_rating(state) >= 4) ) def protoss_hand_of_darkness_requirement(self, state: CollectionState) -> bool: @@ -2412,7 +2416,7 @@ class SC2Logic: return self.protoss_deathball(state) and self.protoss_power_rating(state) >= 8 def zerg_the_reckoning_requirement(self, state: CollectionState) -> bool: - if not (self.zerg_power_rating(state) >= 6 or self.basic_kerrigan(state)): + if not (self.zerg_power_rating(state) >= 6 or self.basic_kerrigan(state, False)): return False if self.take_over_ai_allies: return ( @@ -2460,20 +2464,22 @@ class SC2Logic: def the_infinite_cycle_requirement(self, state: CollectionState) -> bool: return ( - self.grant_story_tech == GrantStoryTech.option_grant - or not self.kerrigan_unit_available - or ( - state.has_any( - ( - item_names.KERRIGAN_KINETIC_BLAST, - item_names.KERRIGAN_SPAWN_BANELINGS, - item_names.KERRIGAN_LEAPING_STRIKE, - item_names.KERRIGAN_SPAWN_LEVIATHAN, - ), - self.player, + self.kerrigan_levels(state, 70) + and ( + self.grant_story_tech == GrantStoryTech.option_grant + or not self.kerrigan_unit_available + or ( + state.has_any( + ( + item_names.KERRIGAN_KINETIC_BLAST, + item_names.KERRIGAN_SPAWN_BANELINGS, + item_names.KERRIGAN_LEAPING_STRIKE, + item_names.KERRIGAN_SPAWN_LEVIATHAN, + ), + self.player, + ) + and self.basic_kerrigan(state) ) - and self.basic_kerrigan(state) - and self.kerrigan_levels(state, 70) ) ) diff --git a/worlds/sc2/test/test_generation.py b/worlds/sc2/test/test_generation.py index 67e302fe..110fa937 100644 --- a/worlds/sc2/test/test_generation.py +++ b/worlds/sc2/test/test_generation.py @@ -2,6 +2,7 @@ Unit tests for world generation """ from typing import * + from .test_base import Sc2SetupTestBase from .. import mission_groups, mission_tables, options, locations, SC2Mission, SC2Campaign, SC2Race, unreleased_items, \ diff --git a/worlds/sc2/test/test_usecases.py b/worlds/sc2/test/test_usecases.py index a87d1766..b5175877 100644 --- a/worlds/sc2/test/test_usecases.py +++ b/worlds/sc2/test/test_usecases.py @@ -6,7 +6,9 @@ from .test_base import Sc2SetupTestBase from .. import get_all_missions, mission_tables, options from ..item import item_groups, item_tables, item_names from ..mission_tables import SC2Race, SC2Mission, SC2Campaign, MissionFlag -from ..options import EnabledCampaigns, MasteryLocations +from ..options import EnabledCampaigns, MasteryLocations, MissionOrder, EnableRaceSwapVariants, ShuffleCampaigns, \ + ShuffleNoBuild, StarterUnit, RequiredTactics, KerriganPresence, KerriganLevelItemDistribution, GrantStoryTech, \ + GrantStoryLevels class TestSupportedUseCases(Sc2SetupTestBase): @@ -490,3 +492,39 @@ class TestSupportedUseCases(Sc2SetupTestBase): self.assertTupleEqual(terran_nonmerc_units, ()) self.assertTupleEqual(zerg_nonmerc_units, ()) + + def test_all_kerrigan_missions_are_nobuild_and_grant_story_tech_is_on(self) -> None: + # The actual situation the bug got caught + world_options = { + 'mission_order': MissionOrder.option_vanilla_shuffled, + 'selected_races': [ + SC2Race.TERRAN.get_title(), + SC2Race.ZERG.get_title(), + SC2Race.PROTOSS.get_title(), + ], + 'enabled_campaigns': [ + SC2Campaign.WOL.campaign_name, + SC2Campaign.PROPHECY.campaign_name, + SC2Campaign.HOTS.campaign_name, + SC2Campaign.PROLOGUE.campaign_name, + SC2Campaign.LOTV.campaign_name, + SC2Campaign.EPILOGUE.campaign_name, + SC2Campaign.NCO.campaign_name, + ], + 'enable_race_swap': EnableRaceSwapVariants.option_shuffle_all_non_vanilla, # Causes no build Kerrigan missions to be present, only nobuilds remain + 'shuffle_campaigns': ShuffleCampaigns.option_true, + 'shuffle_no_build': ShuffleNoBuild.option_true, + 'starter_unit': StarterUnit.option_balanced, + 'required_tactics': RequiredTactics.option_standard, + 'kerrigan_presence': KerriganPresence.option_vanilla, + 'kerrigan_levels_per_mission_completed': 0, + 'kerrigan_levels_per_mission_completed_cap': -1, + 'kerrigan_level_item_sum': 87, + 'kerrigan_level_item_distribution': KerriganLevelItemDistribution.option_size_7, + 'kerrigan_total_level_cap': -1, + 'start_primary_abilities': 0, + 'grant_story_tech': GrantStoryTech.option_grant, + 'grant_story_levels': GrantStoryLevels.option_additive, + } + self.generate_world(world_options) + # Just check that the world itself generates under those rules and no exception is thrown