diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index e390e93a3862..395685ff7d45 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -24,8 +24,8 @@ from worlds.sc2.Options import MissionOrder, KerriganPrimalStatus, kerrigan_unit_available, KerriganPresence, \ GameSpeed, GenericUpgradeItems, GenericUpgradeResearch, ColorChoice, GenericUpgradeMissions, \ LocationInclusion, ExtraLocations, MasteryLocations, ChallengeLocations, VanillaLocations, \ - DisableForcedCamera, SkipCutscenes, GrantStoryTech, TakeOverAIAllies, RequiredTactics, SpearOfAdunPresence, \ - SpearOfAdunPresentInNoBuild, SpearOfAdunAutonomouslyCastAbilityPresence, \ + DisableForcedCamera, SkipCutscenes, GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, RequiredTactics, \ + SpearOfAdunPresence, SpearOfAdunPresentInNoBuild, SpearOfAdunAutonomouslyCastAbilityPresence, \ SpearOfAdunAutonomouslyCastPresentInNoBuild @@ -459,7 +459,10 @@ def on_package(self, cmd: str, args: dict) -> None: self.kerrigan_presence = args["slot_data"].get("kerrigan_presence", KerriganPresence.option_vanilla) self.kerrigan_primal_status = args["slot_data"].get("kerrigan_primal_status", KerriganPrimalStatus.option_vanilla) self.kerrigan_levels_per_mission_completed = args["slot_data"].get("kerrigan_levels_per_mission_completed", 0) + self.kerrigan_levels_per_mission_completed_cap = args["slot_data"].get("kerrigan_levels_per_mission_completed_cap", -1) + self.kerrigan_total_level_cap = args["slot_data"].get("kerrigan_total_level_cap", -1) self.grant_story_tech = args["slot_data"].get("grant_story_tech", GrantStoryTech.option_false) + self.grant_story_levels = args["slot_data"].get("grant_story_levels", GrantStoryLevels.option_additive) self.required_tactics = args["slot_data"].get("required_tactics", RequiredTactics.option_standard) self.take_over_ai_allies = args["slot_data"].get("take_over_ai_allies", TakeOverAIAllies.option_false) self.spear_of_adun_presence = args["slot_data"].get("spear_of_adun_presence", SpearOfAdunPresence.option_not_present) @@ -472,8 +475,9 @@ def on_package(self, cmd: str, args: dict) -> None: self.nova_covert_ops_only = args["slot_data"].get("nova_covert_ops_only", False) if self.required_tactics == RequiredTactics.option_no_logic: - # Locking Grant Story Tech if no logic + # Locking Grant Story Tech/Levels if no logic self.grant_story_tech = GrantStoryTech.option_true + self.grant_story_levels = GrantStoryLevels.option_minimum self.location_inclusions = { LocationType.VICTORY: LocationInclusion.option_enabled, # Victory checks are always enabled @@ -734,9 +738,14 @@ def calc_difficulty(difficulty: int): def get_kerrigan_level(ctx: SC2Context, items: typing.Dict[SC2Race, typing.List[int]], missions_beaten: int) -> int: - value = items[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]] - value += missions_beaten * ctx.kerrigan_levels_per_mission_completed - return value + item_value = items[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]] + mission_value = missions_beaten * ctx.kerrigan_levels_per_mission_completed + if ctx.kerrigan_levels_per_mission_completed_cap != -1: + mission_value = min(mission_value, ctx.kerrigan_levels_per_mission_completed_cap) + total_value = item_value + mission_value + if ctx.kerrigan_total_level_cap != -1: + total_value = min(total_value, ctx.kerrigan_total_level_cap) + return total_value def calculate_kerrigan_options(ctx: SC2Context) -> int: @@ -869,7 +878,7 @@ async def on_step(self, iteration: int): game_speed = self.ctx.game_speed_override else: game_speed = self.ctx.game_speed - await self.chat_send("?SetOptions {} {} {} {} {} {} {} {} {} {} {} {}".format( + await self.chat_send("?SetOptions {} {} {} {} {} {} {} {} {} {} {} {} {}".format( difficulty, self.ctx.generic_upgrade_research, self.ctx.all_in_choice, @@ -881,7 +890,8 @@ async def on_step(self, iteration: int): self.ctx.take_over_ai_allies, soa_options, self.ctx.mission_order, - 1 if self.ctx.nova_covert_ops_only else 0 + 1 if self.ctx.nova_covert_ops_only else 0, + self.ctx.grant_story_levels )) await self.chat_send("?GiveResources {} {} {}".format( start_items[SC2Race.ANY][0], @@ -949,8 +959,6 @@ async def on_step(self, iteration: int): for x, completed in enumerate(self.boni): if not completed and game_state & (1 << (x + 2)): - # Store check amount ahead of time to avoid server changing value mid-calculation - checks = len(self.ctx.checked_locations) await self.ctx.send_msgs( [{"cmd": 'LocationChecks', "locations": [get_location_offset(self.mission_id) + VICTORY_MODULO * self.mission_id + x + 1]}]) @@ -959,7 +967,7 @@ async def on_step(self, iteration: int): await self.chat_send("?SendMessage LostConnection - Lost connection to game.") def missions_beaten_count(self): - return len([location for location in self.ctx.locations_checked if location % VICTORY_MODULO == 0]) + return len([location for location in self.ctx.checked_locations if location % VICTORY_MODULO == 0]) async def updateColors(self): await self.chat_send("?SetColor rr " + str(self.ctx.player_color_raynor)) diff --git a/worlds/sc2/Options.py b/worlds/sc2/Options.py index 8d76a8ee42ee..cab003401c37 100644 --- a/worlds/sc2/Options.py +++ b/worlds/sc2/Options.py @@ -395,13 +395,30 @@ class KerriganLevelsPerMissionCompleted(Range): default = 0 +class KerriganLevelsPerMissionCompletedCap(Range): + """ + Limits how many total levels Kerrigan can gain from beating missions. This does not affect levels gained from items. + Set to -1 to disable this limit. + + NOTE: The following missions have these level requirements: + Supreme: 35 + The Infinite Cycle: 70 + See Grant Story Levels for more details. + """ + display_name = "Levels Per Mission Beaten Cap" + range_start = -1 + range_end = 140 + default = -1 + + class KerriganLevelItemSum(Range): """ - Determines the sum of the level items in the world. This does not affect levels gained from checks. + Determines the sum of the level items in the world. This does not affect levels gained from beating missions. NOTE: The following missions have these level requirements: Supreme: 35 The Infinite Cycle: 70 + See Grant Story Levels for more details. """ display_name = "Kerrigan Level Item Sum" range_start = 0 @@ -415,7 +432,7 @@ class KerriganLevelItemDistribution(Choice): Vanilla: Uses the distribution in the vanilla campaign. This entails 32 individual levels and 6 packs of varying sizes. This distribution always adds up to 70, ignoring the Level Item Sum setting. - Smooth: Uses a custom, condensed distribution of items between sizes 4 and 10, + Smooth: Uses a custom, condensed distribution of 10 items between sizes 4 and 10, intended to fit more levels into settings with little room for filler while keeping some variance in level gains. This distribution always adds up to 70, ignoring the Level Item Sum setting. Size 70: Uses items worth 70 levels each. @@ -441,6 +458,22 @@ class KerriganLevelItemDistribution(Choice): default = option_smooth +class KerriganTotalLevelCap(Range): + """ + Limits how many total levels Kerrigan can gain from any source. Depending on your other settings, + there may be more levels available in the world, but they will not affect Kerrigan. + Set to -1 to disable this limit. + + NOTE: The following missions have these level requirements: + Supreme: 35 + The Infinite Cycle: 70 + See Grant Story Levels for more details. + """ + display_name = "Total Level Cap" + range_start = -1 + range_end = 140 + default = -1 + class StartPrimaryAbilities(Range): """Number of Primary Abilities (Kerrigan Tier 1, 2, and 4) to start the game with. @@ -453,7 +486,7 @@ class StartPrimaryAbilities(Range): class KerriganPrimalStatus(Choice): """Determines when Kerrigan appears in her Primal Zerg form. - This halves her maximum energy, but greatly increases her energy regeneration. + This greatly increases her energy regeneration. Vanilla: Kerrigan is human in missions that canonically appear before The Crucible, and zerg thereafter. @@ -555,6 +588,31 @@ class GrantStoryTech(Toggle): display_name = "Grant Story Tech" +class GrantStoryLevels(Choice): + """ + If enabled, grants Kerrigan the required minimum levels for the following missions: + Supreme: 35 + The Infinite Cycle: 70 + The bonus levels only apply during the listed missions, and can exceed the Total Level Cap. + + If disabled, either of these missions is included, and there are not enough levels in the world, generation may fail. + To prevent this, either increase the amount of levels in the world, or enable this option. + + If disabled and Required Tactics is set to no logic, this option is forced to Minimum. + + Disabled: Kerrigan does not get bonus levels for these missions, + instead the levels must be gained from items or beating missions. + Additive: Kerrigan gains bonus levels equal to the mission's required level. + Minimum: Kerrigan is either at her real level, or at the mission's required level, + depending on which is higher. + """ + display_name = "Grant Story Levels" + option_disabled = 0 + option_additive = 1 + option_minimum = 2 + default = option_minimum + + class TakeOverAIAllies(Toggle): """ On maps supporting this feature allows you to take control over an AI Ally. @@ -739,8 +797,10 @@ class Starcraft2Options(PerGameCommonOptions): generic_upgrade_items: GenericUpgradeItems kerrigan_presence: KerriganPresence kerrigan_levels_per_mission_completed: KerriganLevelsPerMissionCompleted + kerrigan_levels_per_mission_completed_cap: KerriganLevelsPerMissionCompletedCap kerrigan_level_item_sum: KerriganLevelItemSum kerrigan_level_item_distribution: KerriganLevelItemDistribution + kerrigan_total_level_cap: KerriganTotalLevelCap start_primary_abilities: StartPrimaryAbilities kerrigan_primal_status: KerriganPrimalStatus spear_of_adun_presence: SpearOfAdunPresence @@ -748,6 +808,7 @@ class Starcraft2Options(PerGameCommonOptions): spear_of_adun_autonomously_cast_ability_presence: SpearOfAdunAutonomouslyCastAbilityPresence spear_of_adun_autonomously_cast_present_in_no_build: SpearOfAdunAutonomouslyCastPresentInNoBuild grant_story_tech: GrantStoryTech + grant_story_levels: GrantStoryLevels take_over_ai_allies: TakeOverAIAllies locked_items: LockedItems excluded_items: ExcludedItems diff --git a/worlds/sc2/Rules.py b/worlds/sc2/Rules.py index c90be6ccba81..91011f78ed7a 100644 --- a/worlds/sc2/Rules.py +++ b/worlds/sc2/Rules.py @@ -2,7 +2,8 @@ from BaseClasses import MultiWorld, CollectionState from .Options import get_option_value, RequiredTactics, kerrigan_unit_available, AllInMap, \ - GrantStoryTech, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence, get_enabled_campaigns, MissionOrder + GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence, \ + get_enabled_campaigns, MissionOrder from .Items import get_basic_units, defense_ratings, zerg_defense_ratings, kerrigan_actives, air_defense_ratings, \ kerrigan_levels, get_full_item_list from .MissionTables import SC2Race, SC2Campaign @@ -431,18 +432,26 @@ def supreme_requirement(self, state: CollectionState) -> bool: ) def kerrigan_levels(self, state: CollectionState, target: int) -> bool: - if self.story_tech_granted or not self.kerrigan_unit_available: - return True # Levels are granted by story tech - if self.kerrigan_levels_per_mission_completed > 0 and not self.is_item_placement(state): + if self.story_levels_granted or not self.kerrigan_unit_available: + return True # Levels are granted + if self.kerrigan_levels_per_mission_completed > 0 \ + and self.kerrigan_levels_per_mission_completed_cap > 0 \ + and not self.is_item_placement(state): # Levels can be granted from mission completion. # Item pool filtering isn't aware of missions beaten. Assume that missions beaten will fulfill this rule. return True - levels = 0 - levels += self.kerrigan_levels_per_mission_completed * state.count_group("Missions", self.player) + # 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 from items for kerrigan_level_item in kerrigan_levels: level_amount = get_full_item_list()[kerrigan_level_item].number item_count = state.count(kerrigan_level_item, self.player) levels += item_count * level_amount + # Total level cap + if self.kerrigan_total_level_cap != -1: + levels = min(levels, self.kerrigan_total_level_cap) return levels >= target @@ -928,7 +937,10 @@ def __init__(self, multiworld: MultiWorld, player: int): self.kerrigan_unit_available = get_option_value(multiworld, self.player, 'kerrigan_presence') in kerrigan_unit_available \ and SC2Campaign.HOTS in get_enabled_campaigns(multiworld, self.player) self.kerrigan_levels_per_mission_completed = get_option_value(multiworld, self.player, "kerrigan_levels_per_mission_completed") + self.kerrigan_levels_per_mission_completed_cap = get_option_value(multiworld, self.player, "kerrigan_levels_per_mission_completed_cap") + self.kerrigan_total_level_cap = get_option_value(multiworld, self.player, "kerrigan_total_level_cap") self.story_tech_granted = get_option_value(multiworld, self.player, "grant_story_tech") == GrantStoryTech.option_true + self.story_levels_granted = get_option_value(multiworld, self.player, "grant_story_levels") != GrantStoryLevels.option_disabled self.basic_terran_units = get_basic_units(self.multiworld, self.player, SC2Race.TERRAN) self.basic_zerg_units = get_basic_units(self.multiworld, self.player, SC2Race.ZERG) self.basic_protoss_units = get_basic_units(self.multiworld, self.player, SC2Race.PROTOSS)