Skip to content

Commit

Permalink
SC2: Basic race-swap mission logic
Browse files Browse the repository at this point in the history
  • Loading branch information
EnvyDragon committed May 25, 2024
1 parent 284237e commit dea70e3
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 18 deletions.
9 changes: 5 additions & 4 deletions worlds/sc2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from .options import (get_option_value, LocationInclusion, KerriganLevelItemDistribution,
KerriganPresence, KerriganPrimalStatus, kerrigan_unit_available, StarterUnit, SpearOfAdunPresence,
get_enabled_campaigns, SpearOfAdunAutonomouslyCastAbilityPresence, Starcraft2Options,
GrantStoryTech, GenericUpgradeResearch,
GrantStoryTech, GenericUpgradeResearch, get_enabled_races
)
from .pool_filter import filter_items
from .mission_tables import (
Expand Down Expand Up @@ -246,6 +246,7 @@ def resolve_count(count: Optional[int], max_count: int) -> int:
def flag_excludes_by_faction_presence(world: SC2World, item_list: List[FilterItem]) -> None:
"""Excludes items based on if their faction has a mission present where they can be used"""
missions = get_all_missions(world.mission_req_table)
races = get_enabled_races(world)
if world.options.take_over_ai_allies.value:
terran_missions = [mission for mission in missions if (MissionFlag.Terran|MissionFlag.AiTerranAlly) & mission.flags]
zerg_missions = [mission for mission in missions if (MissionFlag.Zerg|MissionFlag.AiZergAlly) & mission.flags]
Expand All @@ -264,13 +265,13 @@ def flag_excludes_by_faction_presence(world: SC2World, item_list: List[FilterIte

for item in item_list:
# Catch-all for all of a faction's items
if (not terran_missions and item.data.race == SC2Race.TERRAN):
if (not terran_missions or SC2Race.TERRAN not in races) and item.data.race == SC2Race.TERRAN:
item.flags |= ItemFilterFlags.Excluded
continue
if (not zerg_missions and item.data.race == SC2Race.ZERG):
if (not zerg_missions or SC2Race.ZERG not in races) and item.data.race == SC2Race.ZERG:
item.flags |= ItemFilterFlags.Excluded
continue
if (not protoss_missions and item.data.race == SC2Race.PROTOSS):
if (not protoss_missions or SC2Race.PROTOSS not in races) and item.data.race == SC2Race.PROTOSS:
if item.name not in item_groups.soa_items:
item.flags |= ItemFilterFlags.Excluded
continue
Expand Down
22 changes: 20 additions & 2 deletions worlds/sc2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
SpearOfAdunPresence, SpearOfAdunPresentInNoBuild, SpearOfAdunAutonomouslyCastAbilityPresence,
SpearOfAdunAutonomouslyCastPresentInNoBuild, LEGACY_GRID_ORDERS,
)
from .mission_tables import MissionFlag


if __name__ == "__main__":
Expand Down Expand Up @@ -1013,6 +1014,21 @@ def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool:
return get_full_item_list()[item_names.KERRIGAN_PRIMAL_FORM].code in codes
return False


def get_mission_variant(mission_id):
mission_flags = lookup_id_to_mission[mission_id].flags
faction_variant = 0
if MissionFlag.RaceSwap not in mission_flags:
return faction_variant
if MissionFlag.Terran in mission_flags:
faction_variant = 1
elif MissionFlag.Zerg in mission_flags:
faction_variant = 2
elif MissionFlag.Protoss in mission_flags:
faction_variant = 3
return faction_variant


async def starcraft_launch(ctx: SC2Context, mission_id: int):
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id].mission_name}. If game does not launch check log file for errors.")

Expand Down Expand Up @@ -1061,6 +1077,7 @@ async def on_step(self, iteration: int):
kerrigan_level = get_kerrigan_level(self.ctx, start_items, missions_beaten)
kerrigan_options = calculate_kerrigan_options(self.ctx)
soa_options = caclulate_soa_options(self.ctx)
mission_variant = get_mission_variant(self.mission_id)
uncollected_objectives: typing.List[int] = self.get_uncollected_objectives()
if self.ctx.difficulty_override >= 0:
difficulty = calc_difficulty(self.ctx.difficulty_override)
Expand All @@ -1070,7 +1087,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,
Expand All @@ -1084,7 +1101,8 @@ async def on_step(self, iteration: int):
self.ctx.mission_order,
1 if self.ctx.nova_covert_ops_only else 0,
self.ctx.grant_story_levels,
self.ctx.enable_morphling
self.ctx.enable_morphling,
mission_variant
))
await self.chat_send("?GiveResources {} {} {}".format(
start_items[SC2Race.ANY][0],
Expand Down
47 changes: 47 additions & 0 deletions worlds/sc2/locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
SC2HOTS_LOC_ID_OFFSET = 20000000 # Avoid clashes with The Legend of Zelda
SC2LOTV_LOC_ID_OFFSET = SC2HOTS_LOC_ID_OFFSET + 2000
SC2NCO_LOC_ID_OFFSET = SC2LOTV_LOC_ID_OFFSET + 2500
SC2_RACESWAP_LOC_ID_OFFSET = SC2NCO_LOC_ID_OFFSET + 900


class SC2Location(Location):
Expand Down Expand Up @@ -1619,6 +1620,52 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]:
lambda state: logic.end_game_requirement(state) and logic.nova_any_weapon(state)),
LocationData("End Game", "End Game: Xanthos", SC2NCO_LOC_ID_OFFSET + 901, LocationType.VANILLA,
lambda state: logic.end_game_requirement(state)),

# Mission Variants
LocationData("Smash and Grab (Z)", "Smash and Grab (Zerg): Victory", SC2_RACESWAP_LOC_ID_OFFSET + 100, LocationType.VICTORY,
lambda state: logic.zerg_common_unit(state) and
(adv_tactics and logic.zerg_basic_anti_air(state)
or logic.zerg_competent_anti_air(state))),
LocationData("Smash and Grab (Z)", "Smash and Grab (Zerg): First Relic", SC2_RACESWAP_LOC_ID_OFFSET + 101, LocationType.VANILLA),
LocationData("Smash and Grab (Z)", "Smash and Grab (Zerg): Second Relic", SC2_RACESWAP_LOC_ID_OFFSET + 102, LocationType.VANILLA),
LocationData("Smash and Grab (Z)", "Smash and Grab (Zerg): Third Relic", SC2_RACESWAP_LOC_ID_OFFSET + 103, LocationType.VANILLA,
lambda state: logic.zerg_common_unit(state) and
(adv_tactics and logic.zerg_basic_kerriganless_anti_air(state)
or logic.zerg_competent_anti_air(state))),
LocationData("Smash and Grab (Z)", "Smash and Grab (Zerg): Fourth Relic", SC2_RACESWAP_LOC_ID_OFFSET + 104, LocationType.VANILLA,
lambda state: logic.zerg_common_unit(state) and
(adv_tactics and logic.zerg_basic_kerriganless_anti_air(state)
or logic.zerg_competent_anti_air(state))),
LocationData("Smash and Grab (Z)", "Smash and Grab (Zerg): First Forcefield Area Busted", SC2_RACESWAP_LOC_ID_OFFSET + 105, LocationType.EXTRA,
lambda state: logic.zerg_common_unit(state) and
(adv_tactics and logic.zerg_basic_kerriganless_anti_air(state)
or logic.zerg_competent_anti_air(state))),
LocationData("Smash and Grab (Z)", "Smash and Grab (Zerg): Second Forcefield Area Busted", SC2_RACESWAP_LOC_ID_OFFSET + 106, LocationType.EXTRA,
lambda state: logic.zerg_common_unit(state) and
(adv_tactics and logic.zerg_basic_kerriganless_anti_air(state)
or logic.zerg_competent_anti_air(state))),
LocationData("Smash and Grab (P)", "Smash and Grab (Protoss): Victory", SC2_RACESWAP_LOC_ID_OFFSET + 200, LocationType.VICTORY,
lambda state: logic.protoss_common_unit(state) and
(adv_tactics and logic.protoss_basic_anti_air(state)
or logic.protoss_competent_anti_air(state))),
LocationData("Smash and Grab (P)", "Smash and Grab (Protoss): First Relic", SC2_RACESWAP_LOC_ID_OFFSET + 201, LocationType.VANILLA),
LocationData("Smash and Grab (P)", "Smash and Grab (Protoss): Second Relic", SC2_RACESWAP_LOC_ID_OFFSET + 202, LocationType.VANILLA),
LocationData("Smash and Grab (P)", "Smash and Grab (Protoss): Third Relic", SC2_RACESWAP_LOC_ID_OFFSET + 203, LocationType.VANILLA,
lambda state: logic.protoss_common_unit(state) and
(adv_tactics and logic.protoss_basic_anti_air(state)
or logic.protoss_competent_anti_air(state))),
LocationData("Smash and Grab (P)", "Smash and Grab (Protoss): Fourth Relic", SC2_RACESWAP_LOC_ID_OFFSET + 204, LocationType.VANILLA,
lambda state: logic.protoss_common_unit(state) and
(adv_tactics and logic.protoss_basic_anti_air(state)
or logic.protoss_competent_anti_air(state))),
LocationData("Smash and Grab (P)", "Smash and Grab (Protoss): First Forcefield Area Busted", SC2_RACESWAP_LOC_ID_OFFSET + 205, LocationType.EXTRA,
lambda state: logic.protoss_common_unit(state) and
(adv_tactics and logic.protoss_basic_anti_air(state)
or logic.protoss_competent_anti_air(state))),
LocationData("Smash and Grab (P)", "Smash and Grab (Protoss): Second Forcefield Area Busted", SC2_RACESWAP_LOC_ID_OFFSET + 206, LocationType.EXTRA,
lambda state: logic.protoss_common_unit(state) and
(adv_tactics and logic.protoss_basic_anti_air(state)
or logic.protoss_competent_anti_air(state))),
]

beat_events = []
Expand Down
9 changes: 7 additions & 2 deletions worlds/sc2/mission_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class MissionFlag(IntFlag):
VsTerran = auto()
VsZerg = auto()
VsProtoss = auto()
RaceSwap = auto() # The mission uses a faction other than the one it uses in vanilla

AiAlly = AiTerranAlly|AiZergAlly|AiProtossAlly
TimedDefense = AutoScroller|Defense
Expand Down Expand Up @@ -194,6 +195,10 @@ def __init__(self, mission_id: int, name: str, campaign: SC2Campaign, area: str,
DARK_SKIES = 82, "Dark Skies", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.HARD, "ap_dark_skies", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.TimedDefense|MissionFlag.VsProtoss
END_GAME = 83, "End Game", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_end_game", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.Defense|MissionFlag.VsTerran

# Race-Swapped Variants
SMASH_AND_GRAB_Z = 84, "Smash and Grab (Z)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.EASY, "ap_smash_and_grab", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsPZ|MissionFlag.RaceSwap
SMASH_AND_GRAB_P = 85, "Smash and Grab (P)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.EASY, "ap_smash_and_grab", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsPZ|MissionFlag.RaceSwap


class MissionConnection:
campaign: SC2Campaign
Expand Down Expand Up @@ -486,5 +491,5 @@ def get_campaign_potential_goal_missions(campaign: SC2Campaign) -> List[SC2Missi
return missions


def get_no_build_missions() -> List[SC2Mission]:
return [mission for mission in SC2Mission if MissionFlag.NoBuild in mission.flags]
def get_missions_with_flags(flags: MissionFlag) -> List[SC2Mission]:
return [mission for mission in SC2Mission if flags & mission.flags]
81 changes: 75 additions & 6 deletions worlds/sc2/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
PerGameCommonOptions, Option, VerifyKeys)
from Utils import get_fuzzy_results
from BaseClasses import PlandoOptions
from .mission_tables import SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_no_build_missions, \
campaign_mission_table
from .mission_tables import SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_missions_with_flags, \
campaign_mission_table, SC2Race, MissionFlag
from .mission_orders import vanilla_shuffle_order, mini_campaign_order
from .mission_groups import mission_groups, MissionGroupNames

Expand Down Expand Up @@ -133,7 +133,7 @@ class MaximumCampaignSize(Range):
"""
display_name = "Maximum Campaign Size"
range_start = 1
range_end = 83
range_end = 85
default = 83


Expand Down Expand Up @@ -188,6 +188,23 @@ class PlayerColorZergPrimal(ColorChoice):
display_name = "Zerg Player Color (Primal)"


class SelectRaces(Choice):
"""
Pick which factions' missions can be shuffled into the world.
"""
# bit 0: terran, bit 1: zerg, bit 2: protoss. all disabled means plando only
display_name = "Select Playable Races"
option_all = 7
option_terran = 1
option_zerg = 2
option_protoss = 4
option_terran_and_zerg = 3
option_terran_and_protoss = 5
option_zerg_and_protoss = 6
option_plando = 0
default = option_all


class EnableWolMissions(DefaultOnToggle):
"""
Enables missions from main Wings of Liberty campaign.
Expand Down Expand Up @@ -244,6 +261,22 @@ class EnableNCOMissions(DefaultOnToggle):
display_name = "Enable Nova Covert Ops missions"


class EnableRaceSwapVariants(Choice):
"""
Allow mission variants where you play a faction other than the one the map was initially
designed for.
Disabled: Don't shuffle any non-vanilla map variants into the pool.
Pick One: Only allow one version of each map at a time
Shuffle All: Each version of a map can appear in the same pool (so a map can appear up to 3 times as different races)
"""
display_name = "Enable Race-Swapped Mission Variants"
option_disabled = 0
option_pick_one = 1
option_shuffle_all = 2
default = option_disabled


class ShuffleCampaigns(DefaultOnToggle):
"""
Shuffles the missions between campaigns if enabled.
Expand Down Expand Up @@ -886,13 +919,15 @@ class Starcraft2Options(PerGameCommonOptions):
player_color_protoss: PlayerColorProtoss
player_color_zerg: PlayerColorZerg
player_color_zerg_primal: PlayerColorZergPrimal
selected_races: SelectRaces
enable_wol_missions: EnableWolMissions
enable_prophecy_missions: EnableProphecyMissions
enable_hots_missions: EnableHotsMissions
enable_lotv_prologue_missions: EnableLotVPrologueMissions
enable_lotv_missions: EnableLotVMissions
enable_epilogue_missions: EnableEpilogueMissions
enable_nco_missions: EnableNCOMissions
enable_race_swap: EnableRaceSwapVariants
shuffle_campaigns: ShuffleCampaigns
shuffle_no_build: ShuffleNoBuild
starter_unit: StarterUnit
Expand Down Expand Up @@ -944,6 +979,21 @@ def get_option_value(world: 'SC2World', name: str) -> Union[int, FrozenSet]:
return player_option.value


def get_enabled_races(world: 'SC2World') -> Set[SC2Race]:
selection = world.options.selected_races.value
enabled = set()
# if bit 0, enable terran
if selection & 1:
enabled.add(SC2Race.TERRAN)
# if bit 1, enable zerg
if selection & (1 << 1):
enabled.add(SC2Race.ZERG)
# if bit 2, enable protoss
if selection & (1 << 2):
enabled.add(SC2Race.PROTOSS)
return enabled


def get_enabled_campaigns(world: 'SC2World') -> Set[SC2Campaign]:
enabled_campaigns = set()
if get_option_value(world, "enable_wol_missions"):
Expand Down Expand Up @@ -971,11 +1021,30 @@ def get_disabled_campaigns(world: 'SC2World') -> Set[SC2Campaign]:
return disabled_campaigns


def get_disabled_flags(world: 'SC2World') -> MissionFlag:
excluded = 0
races = get_enabled_races(world)
# filter out missions based on disabled races
if SC2Race.TERRAN not in races:
excluded |= MissionFlag.Terran
if SC2Race.ZERG not in races:
excluded |= MissionFlag.Zerg
if SC2Race.PROTOSS not in races:
excluded |= MissionFlag.Protoss
# filter out race-swapped mission variants
if world.options.enable_race_swap == EnableRaceSwapVariants.option_disabled:
excluded |= MissionFlag.RaceSwap
# filter out no-build missions
if not world.options.shuffle_no_build:
excluded |= MissionFlag.NoBuild
return MissionFlag(excluded)


def get_excluded_missions(world: 'SC2World') -> Set[SC2Mission]:
mission_order_type = world.options.mission_order.value
excluded_mission_names = world.options.excluded_missions.value
shuffle_no_build = world.options.shuffle_no_build.value
disabled_campaigns = get_disabled_campaigns(world)
disabled_flags = get_disabled_flags(world)

excluded_missions: Set[SC2Mission] = set([lookup_name_to_mission[name] for name in excluded_mission_names])

Expand All @@ -991,8 +1060,8 @@ def get_excluded_missions(world: 'SC2World') -> Set[SC2Mission]:
mission.pool == MissionPools.VERY_HARD and mission.campaign != SC2Campaign.EPILOGUE]
)
# Omitting No-Build missions if not shuffling no-build
if not shuffle_no_build:
excluded_missions = excluded_missions.union(get_no_build_missions())
if disabled_flags:
excluded_missions = excluded_missions.union(get_missions_with_flags(disabled_flags))
# Omitting missions not in enabled campaigns
for campaign in disabled_campaigns:
excluded_missions = excluded_missions.union(campaign_mission_table[campaign])
Expand Down
11 changes: 7 additions & 4 deletions worlds/sc2/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from BaseClasses import CollectionState
from .options import get_option_value, RequiredTactics, kerrigan_unit_available, AllInMap, \
GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence, \
get_enabled_campaigns, MissionOrder, EnableMorphling
get_enabled_campaigns, MissionOrder, EnableMorphling, get_enabled_races
from .items import get_basic_units, defense_ratings, zerg_defense_ratings, kerrigan_actives, air_defense_ratings, \
kerrigan_levels, get_full_item_list
from .mission_tables import SC2Race, SC2Campaign
Expand Down Expand Up @@ -340,9 +340,12 @@ def zerg_competent_anti_air(self, state: CollectionState) -> bool:
or (self.advanced_tactics and state.has(item_names.INFESTOR, self.player))

def zerg_basic_anti_air(self, state: CollectionState) -> bool:
return self.zerg_competent_anti_air(state) or self.kerrigan_unit_available in kerrigan_unit_available or \
state.has_any({item_names.SWARM_QUEEN, item_names.SCOURGE}, self.player) or (self.advanced_tactics and state.has(item_names.SPORE_CRAWLER, self.player))

return self.zerg_basic_kerriganless_anti_air(state) or self.kerrigan_unit_available in kerrigan_unit_available

def zerg_basic_kerriganless_anti_air(self, state: CollectionState) -> bool:
return self.zerg_competent_anti_air(state) or state.has_any({item_names.SWARM_QUEEN, item_names.SCOURGE}, self.player) \
or (self.advanced_tactics and state.has(item_names.SPORE_CRAWLER, self.player))

def morph_brood_lord(self, state: CollectionState) -> bool:
return (state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player) or self.morphling_enabled) \
and state.has(item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, self.player)
Expand Down

0 comments on commit dea70e3

Please sign in to comment.