diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index dc8ed80e8e3e..2383b1994bab 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -9,7 +9,7 @@ from worlds.AutoWorld import WebWorld, World from . import item_names from .items import ( - StarcraftItem, filler_items, get_full_item_list, + StarcraftItem, filler_items, get_full_item_list, ProtossItemType, get_basic_units, ItemData, upgrade_included_names, kerrigan_actives, kerrigan_passives, not_balanced_starting_units, ) @@ -129,6 +129,7 @@ def create_items(self): flag_start_inventory(self, item_list) flag_unused_upgrade_types(self, item_list) flag_user_excluded_item_sets(self, item_list) + flag_war_council_excludes(self, item_list) flag_and_add_resource_locations(self, item_list) pool: List[Item] = prune_item_pool(self, item_list) pad_item_pool_with_filler(self, len(self.location_cache) - len(self.locked_locations) - len(pool), pool) @@ -590,6 +591,16 @@ def flag_user_excluded_item_sets(world: SC2World, item_list: List[FilterItem]) - item.flags |= ItemFilterFlags.Excluded vanilla_nonprogressive_count[item.name] += 1 +def flag_war_council_excludes(world: SC2World, item_list: List[FilterItem]) -> None: + """Excludes items based on item set options (`only_vanilla_items`)""" + if world.options.allow_unit_nerfs: + return + + for item in item_list: + if item.data.type != ProtossItemType.War_Council: + continue + item.flags |= ItemFilterFlags.Excluded + def flag_and_add_resource_locations(world: SC2World, item_list: List[FilterItem]) -> None: """ diff --git a/worlds/sc2/client.py b/worlds/sc2/client.py index b24d20f8f563..31f0e83b593d 100644 --- a/worlds/sc2/client.py +++ b/worlds/sc2/client.py @@ -32,7 +32,7 @@ LocationInclusion, ExtraLocations, MasteryLocations, ChallengeLocations, VanillaLocations, DisableForcedCamera, SkipCutscenes, GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, RequiredTactics, SpearOfAdunPresence, SpearOfAdunPresentInNoBuild, SpearOfAdunAutonomouslyCastAbilityPresence, - SpearOfAdunAutonomouslyCastPresentInNoBuild, LEGACY_GRID_ORDERS, + SpearOfAdunAutonomouslyCastPresentInNoBuild, AllowUnitNerfs, LEGACY_GRID_ORDERS, ) @@ -49,12 +49,14 @@ from worlds._sc2common.bot.player import Bot from .items import ( lookup_id_to_name, get_full_item_list, ItemData, - race_to_item_type, upgrade_item_types, ZergItemType, upgrade_bundles, upgrade_included_names, + race_to_item_type, ZergItemType, ProtossItemType, upgrade_bundles, upgrade_included_names, WEAPON_ARMOR_UPGRADE_MAX_LEVEL, ) from .locations import SC2WOL_LOC_ID_OFFSET, LocationType, SC2HOTS_LOC_ID_OFFSET -from .mission_tables import lookup_id_to_mission, SC2Campaign, lookup_name_to_mission, \ +from .mission_tables import ( + lookup_id_to_mission, SC2Campaign, lookup_name_to_mission, lookup_id_to_campaign, MissionConnection, SC2Mission, campaign_mission_table, SC2Race +) from .regions import MissionInfo import colorama @@ -327,6 +329,7 @@ def _cmd_option(self, option_name: str = "", option_value: str = "") -> None: ConfigurableOptionInfo('no_forced_camera', 'disable_forced_camera', options.DisableForcedCamera), ConfigurableOptionInfo('skip_cutscenes', 'skip_cutscenes', options.SkipCutscenes), ConfigurableOptionInfo('enable_morphling', 'enable_morphling', options.EnableMorphling, can_break_logic=True), + ConfigurableOptionInfo('unit_nerfs', 'allow_unit_nerfs', options.AllowUnitNerfs, can_break_logic=True), ) WARNING_COLOUR = "salmon" @@ -540,6 +543,7 @@ def __init__(self, *args, **kwargs) -> None: self.kerrigan_presence: int = KerriganPresence.default self.kerrigan_primal_status = 0 self.enable_morphling = EnableMorphling.default + self.allow_unit_nerfs: int = AllowUnitNerfs.default self.mission_req_table: typing.Dict[SC2Campaign, typing.Dict[str, MissionInfo]] = {} self.final_mission: int = 29 self.announcements: queue.Queue = queue.Queue() @@ -647,6 +651,7 @@ def on_package(self, cmd: str, args: dict) -> None: self.vespene_per_item = args["slot_data"].get("vespene_per_item", 15) self.starting_supply_per_item = args["slot_data"].get("starting_supply_per_item", 2) self.nova_covert_ops_only = args["slot_data"].get("nova_covert_ops_only", False) + self.allow_unit_nerfs = args["slot_data"].get("allow_unit_nerfs", AllowUnitNerfs.default) if self.required_tactics == RequiredTactics.option_no_logic: # Locking Grant Story Tech/Levels if no logic @@ -890,6 +895,10 @@ def calculate_items(ctx: SC2Context) -> typing.Dict[SC2Race, typing.List[int]]: shield_upgrade_item = item_list[item_names.PROGRESSIVE_PROTOSS_SHIELDS] for _ in range(0, shield_upgrade_level): accumulators[shield_upgrade_item.race][shield_upgrade_item.type.flag_word] += 1 << shield_upgrade_item.number + + # War council option + if not ctx.allow_unit_nerfs: + accumulators[SC2Race.PROTOSS][ProtossItemType.War_Council.flag_word] = (1 << 30) - 1 # Deprecated Orbital Command handling (Backwards compatibility): if orbital_command_count > 0: @@ -1103,23 +1112,24 @@ 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( - difficulty, - self.ctx.generic_upgrade_research, - self.ctx.all_in_choice, - game_speed, - self.ctx.disable_forced_camera, - self.ctx.skip_cutscenes, - kerrigan_options, - self.ctx.grant_story_tech, - self.ctx.take_over_ai_allies, - soa_options, - self.ctx.mission_order, - 1 if self.ctx.nova_covert_ops_only else 0, - self.ctx.grant_story_levels, - self.ctx.enable_morphling, - mission_variant - )) + await self.chat_send( + "?SetOptions" + f" {difficulty}" + f" {self.ctx.generic_upgrade_research}" + f" {self.ctx.all_in_choice}" + f" {game_speed}" + f" {self.ctx.disable_forced_camera}" + f" {self.ctx.skip_cutscenes}" + f" {kerrigan_options}" + f" {self.ctx.grant_story_tech}" + f" {self.ctx.take_over_ai_allies}" + f" {soa_options}" + f" {self.ctx.mission_order}" + f" {int(self.ctx.nova_covert_ops_only)}" + f" {self.ctx.grant_story_levels}" + f" {self.ctx.enable_morphling}" + f" {mission_variant}" + ) await self.chat_send("?GiveResources {} {} {}".format( start_items[SC2Race.ANY][0], start_items[SC2Race.ANY][1], @@ -1220,17 +1230,11 @@ async def updateZergTech(self, current_items, kerrigan_level): zerg_items = current_items[SC2Race.ZERG] kerrigan_primal_by_items = kerrigan_primal(self.ctx, kerrigan_level) kerrigan_primal_bot_value = 1 if kerrigan_primal_by_items else 0 - await self.chat_send("?GiveZergTech {} {} {} {} {} {} {} {} {} {} {} {}".format( - kerrigan_level, kerrigan_primal_bot_value, zerg_items[0], zerg_items[1], zerg_items[2], - zerg_items[3], zerg_items[4], zerg_items[5], zerg_items[6], zerg_items[9], zerg_items[10], zerg_items[11] - )) + await self.chat_send(f"?GiveZergTech {kerrigan_level} {kerrigan_primal_bot_value} " + ' '.join(map(str, zerg_items))) async def updateProtossTech(self, current_items): protoss_items = current_items[SC2Race.PROTOSS] - await self.chat_send("?GiveProtossTech {} {} {} {} {} {} {} {} {} {}".format( - protoss_items[0], protoss_items[1], protoss_items[2], protoss_items[3], protoss_items[4], - protoss_items[5], protoss_items[6], protoss_items[7], protoss_items[8], protoss_items[9] - )) + await self.chat_send("?GiveProtossTech " + " ".join(map(str, protoss_items))) def request_unfinished_missions(ctx: SC2Context) -> None: diff --git a/worlds/sc2/item_descriptions.py b/worlds/sc2/item_descriptions.py index 7675bf66e583..306cbc7d8943 100644 --- a/worlds/sc2/item_descriptions.py +++ b/worlds/sc2/item_descriptions.py @@ -63,6 +63,10 @@ item_names.REAVER: (100, 100, 2), DISPLAY_NAME_CLOAKED_ASSASSIN: (0, 50, 0), item_names.SCOUT: (125, 25, 1), + + # War Council + item_names.CENTURION: (0, 50, 0), + item_names.SENTINEL: (60, 0, 1), } @@ -831,6 +835,10 @@ def _ability_desc(unit_name_plural: str, ability_name: str, ability_description: item_names.HAVOC_BLOODSHARD_RESONANCE: "Havoc 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.", + item_names.CENTURION_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.CENTURION), + item_names.SENTINEL_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.SENTINEL), + item_names.STALKER_PHASE_REACTOR: "Stalkers restore 80 shields over 5 seconds after they Blink.", item_names.SOA_CHRONO_SURGE: "The Spear of Adun increases a target structure's unit warp in and research speeds by +1000% for 20 seconds.", item_names.SOA_PROGRESSIVE_PROXY_PYLON: inspect.cleandoc(""" Level 1: The Spear of Adun quickly warps in a Pylon to a target location. diff --git a/worlds/sc2/item_groups.py b/worlds/sc2/item_groups.py index fbaac30f1fd8..e59e7ee7b1d5 100644 --- a/worlds/sc2/item_groups.py +++ b/worlds/sc2/item_groups.py @@ -150,6 +150,7 @@ class ItemGroupNames: SOA_ITEMS = "SOA" PROTOSS_GLOBAL_UPGRADES = "Protoss Global Upgrades" PROTOSS_BUILDINGS = "Protoss Buildings" + WAR_COUNCIL = "Protoss War Council Upgrades" AIUR_UNITS = "Aiur" NERAZIM_UNITS = "Nerazim" TAL_DARIM_UNITS = "Tal'Darim" @@ -587,3 +588,7 @@ def get_all_group_names(cls) -> typing.Set[str]: item_name_groups[ItemGroupNames.VANILLA_ITEMS] = vanilla_items = ( vanilla_wol_items + vanilla_hots_items + vanilla_lotv_items ) + +item_name_groups[ItemGroupNames.WAR_COUNCIL] = [ + item_name for item_name, item_data in items.item_table.items() if item_data.type == items.ProtossItemType.War_Council +] diff --git a/worlds/sc2/item_names.py b/worlds/sc2/item_names.py index 52910e5a2558..b21fec973f3a 100644 --- a/worlds/sc2/item_names.py +++ b/worlds/sc2/item_names.py @@ -669,6 +669,12 @@ ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS = "Leg Enhancements (Zealot/Sentinel/Centurion)" ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY = "Shield Capacity (Zealot/Sentinel/Centurion)" +# War Council +ZEALOT_WHIRLWIND = "Whirlwind (Zealot)" +CENTURION_RESOURCE_EFFICIENCY = "Resource Efficiency (Centurion)" +SENTINEL_RESOURCE_EFFICIENCY = "Resource Efficiency (Sentinel)" +STALKER_PHASE_REACTOR = "Phase Reactor (Stalker)" + # Spear Of Adun SOA_CHRONO_SURGE = "Chrono Surge (Spear of Adun Calldown)" SOA_PROGRESSIVE_PROXY_PYLON = "Progressive Proxy Pylon (Spear of Adun Calldown)" diff --git a/worlds/sc2/items.py b/worlds/sc2/items.py index 17a83d3be914..fd7b96d6b6e0 100644 --- a/worlds/sc2/items.py +++ b/worlds/sc2/items.py @@ -77,6 +77,7 @@ class ProtossItemType(ItemTypeEnum): """General Protoss unit upgrades""" Forge_3 = "Forge", 9 """General Protoss unit upgrades""" + War_Council = "War Council", 10 class FactionlessItemType(ItemTypeEnum): @@ -1651,6 +1652,12 @@ def get_full_item_list(): item_names.ORACLE_BOSONIC_CORE: ItemData(378 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 18, SC2Race.PROTOSS, origin={"ext"}, parent_item=item_names.ORACLE), item_names.SCOUT_RESOURCE_EFFICIENCY: ItemData(379 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 19, SC2Race.PROTOSS, origin={"ext"}, parent_item=item_names.SCOUT), + # War Council + item_names.ZEALOT_WHIRLWIND: ItemData(500 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 0, SC2Race.PROTOSS, parent_item=item_names.ZEALOT), + item_names.CENTURION_RESOURCE_EFFICIENCY: ItemData(501 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 1, SC2Race.PROTOSS, parent_item=item_names.CENTURION), + item_names.SENTINEL_RESOURCE_EFFICIENCY: ItemData(502 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 2, SC2Race.PROTOSS, parent_item=item_names.SENTINEL), + item_names.STALKER_PHASE_REACTOR: ItemData(503 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 3, SC2Race.PROTOSS, parent_item=item_names.STALKER), + # SoA Calldown powers item_names.SOA_CHRONO_SURGE: ItemData(700 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 0, SC2Race.PROTOSS, origin={"lotv"}), item_names.SOA_PROGRESSIVE_PROXY_PYLON: ItemData(701 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Progressive, 0, SC2Race.PROTOSS, origin={"lotv"}, quantity=2), diff --git a/worlds/sc2/options.py b/worlds/sc2/options.py index 0376ed4507b0..3197733f5446 100644 --- a/worlds/sc2/options.py +++ b/worlds/sc2/options.py @@ -556,6 +556,14 @@ class EnableMorphling(Toggle): display_name = "Enable Morphling" +class AllowUnitNerfs(Toggle): + """ + Controls whether some units can initially be found in a nerfed state, with upgrades restoring their stronger power level. + For example, nerfed Zealots will lack the whirlwind upgrade until it is found as an item. + """ + display_name = "Allow Unit Nerfs" + + class SpearOfAdunPresence(Choice): """ Determines in which missions Spear of Adun calldowns will be available. @@ -929,6 +937,7 @@ class Starcraft2Options(PerGameCommonOptions): start_primary_abilities: StartPrimaryAbilities kerrigan_primal_status: KerriganPrimalStatus enable_morphling: EnableMorphling + allow_unit_nerfs: AllowUnitNerfs spear_of_adun_presence: SpearOfAdunPresence spear_of_adun_present_in_no_build: SpearOfAdunPresentInNoBuild spear_of_adun_autonomously_cast_ability_presence: SpearOfAdunAutonomouslyCastAbilityPresence diff --git a/worlds/sc2/test/test_generation.py b/worlds/sc2/test/test_generation.py index 3f4ed655d181..c457af8ab9dd 100644 --- a/worlds/sc2/test/test_generation.py +++ b/worlds/sc2/test/test_generation.py @@ -471,3 +471,24 @@ def test_planetary_orbital_module_not_present_without_cc_spells(self) -> None: self.assertTrue(itempool) self.assertIn(item_names.PLANETARY_FORTRESS, itempool) self.assertNotIn(item_names.PLANETARY_FORTRESS_ORBITAL_MODULE, itempool) + + def test_disabling_unit_nerfs_removes_war_council_upgrades(self) -> None: + world_options = { + 'enable_wol_missions': False, + 'enable_prophecy_missions': True, + 'enable_hots_missions': False, + 'enable_lotv_prologue_missions': True, + 'enable_lotv_missions': True, + 'enable_epilogue_missions': False, + 'enable_nco_missions': False, + 'mission_order': options.MissionOrder.option_grid, + 'allow_unit_nerfs': options.AllowUnitNerfs.option_false, + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + war_council_item_names = set(item_groups.item_name_groups[item_groups.ItemGroupNames.WAR_COUNCIL]) + present_war_council_items = war_council_item_names.intersection(itempool) + + self.assertTrue(itempool) + self.assertFalse(present_war_council_items, f'Found war council upgrades when allow_unit_nerfs is false: {present_war_council_items}')