Skip to content

Commit

Permalink
Merge pull request Ziktofel#195 from Magnemania/sc2-golden-path
Browse files Browse the repository at this point in the history
Scalable Mission Orders + Golden Path and Hopscotch Mission Orders
  • Loading branch information
Ziktofel authored May 13, 2024
2 parents 648970a + 9894e5d commit f0325e7
Show file tree
Hide file tree
Showing 7 changed files with 468 additions and 313 deletions.
6 changes: 3 additions & 3 deletions worlds/sc2/Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
LocationInclusion, ExtraLocations, MasteryLocations, ChallengeLocations, VanillaLocations,
DisableForcedCamera, SkipCutscenes, GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, RequiredTactics,
SpearOfAdunPresence, SpearOfAdunPresentInNoBuild, SpearOfAdunAutonomouslyCastAbilityPresence,
SpearOfAdunAutonomouslyCastPresentInNoBuild,
SpearOfAdunAutonomouslyCastPresentInNoBuild, LEGACY_GRID_ORDERS,
)


Expand Down Expand Up @@ -1376,8 +1376,8 @@ def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete
else:
req_success = False

# Grid-specific logic (to avoid long path checks and infinite recursion)
if ctx.mission_order in (MissionOrder.option_grid, MissionOrder.option_mini_grid, MissionOrder.option_medium_grid):
# Grid and Blitz-specific logic (to avoid long path checks and infinite recursion)
if ctx.mission_order in (MissionOrder.option_grid, MissionOrder.option_blitz) or (ctx.slot_data_version <= 3 and ctx.mission_order in LEGACY_GRID_ORDERS):
if req_success:
return True
else:
Expand Down
392 changes: 392 additions & 0 deletions worlds/sc2/MissionOrders.py

Large diffs are not rendered by default.

276 changes: 2 additions & 274 deletions worlds/sc2/MissionTables.py

Large diffs are not rendered by default.

60 changes: 34 additions & 26 deletions worlds/sc2/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from BaseClasses import PlandoOptions
from .MissionTables import SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_no_build_missions, \
campaign_mission_table
from .MissionOrders import vanilla_shuffle_order, mini_campaign_order

if TYPE_CHECKING:
from worlds.AutoWorld import World
Expand Down Expand Up @@ -63,29 +64,25 @@ class AllInMap(Choice):

class MissionOrder(Choice):
"""
Determines the order the missions are played in. The last three mission orders end in a random mission.
Determines the order the missions are played in. The first three mission orders ignore the Maximum Campaign Size option.
Vanilla (83 total if all campaigns enabled): Keeps the standard mission order and branching from the vanilla Campaigns.
Vanilla Shuffled (83 total if all campaigns enabled): Keeps same branching paths from the vanilla Campaigns but randomizes the order of missions within.
Mini Campaign (47 total if all campaigns enabled): Shorter version of the campaign with randomized missions and optional branches.
Medium Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards bottom-right mission to win.
Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win.
Blitz (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win.
Gauntlet (7): Linear series of 7 random missions to complete the campaign.
Mini Gauntlet (4): Linear series of 4 random missions to complete the campaign.
Tiny Grid (4): A 2x2 version of Grid. Complete the bottom-right mission to win.
Grid (variable): A grid that will resize to use all non-excluded missions. Corners may be omitted to make the grid more square. Complete the bottom-right mission to win.
Blitz: Missions are divided into sets. Complete one mission from a set to advance to the next set.
Gauntlet: A linear path of missions to complete the campaign.
Grid: Missions are arranged into a grid. Completing a mission unlocks the adjacent missions. Corners may be omitted to make the grid more square. Complete the bottom-right mission to win.
Golden Path: A required line of missions with several optional branches, similar to the Wings of Liberty campaign.
Hopscotch: Missions alternate between mandatory missions and pairs of optional missions.
"""
display_name = "Mission Order"
option_vanilla = 0
option_vanilla_shuffled = 1
option_mini_campaign = 2
option_medium_grid = 3
option_mini_grid = 4
option_blitz = 5
option_gauntlet = 6
option_mini_gauntlet = 7
option_tiny_grid = 8
option_grid = 9
option_golden_path = 10
option_hopscotch = 11


class MaximumCampaignSize(Range):
Expand All @@ -101,8 +98,8 @@ class MaximumCampaignSize(Range):

class GridTwoStartPositions(Toggle):
"""
If turned on and 'grid' mission order is selected, removes a mission from the starting
corner sets the adjacent two missions as the starter missions.
If turned on and 'grid' or 'hopscotch' mission orders are selected,
removes a mission from the starting corner and sets the adjacent two missions as the starter missions.
"""
display_name = "Start with two unlocked missions on grid"
default = Toggle.option_false
Expand Down Expand Up @@ -638,7 +635,7 @@ def from_any(cls, data: Union[List[str], Dict[str, int]]) -> 'Sc2ItemDict':
return cls(data)
else:
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")

def verify(self, world: Type['World'], player_name: str, plando_options: PlandoOptions) -> None:
"""Overridden version of function from Options.VerifyKeys for a better error message"""
new_value: dict[str, int] = {}
Expand All @@ -661,7 +658,7 @@ def get_option_name(self, value):

def __getitem__(self, item: str) -> int:
return self.value.__getitem__(item)

def __iter__(self) -> Iterator[str]:
return self.value.__iter__()

Expand Down Expand Up @@ -928,16 +925,11 @@ def get_excluded_missions(world: 'SC2World') -> Set[SC2Mission]:
excluded_missions: Set[SC2Mission] = set([lookup_name_to_mission[name] for name in excluded_mission_names])

# Excluding Very Hard missions depending on options
if (world.options.exclude_very_hard_missions == ExcludeVeryHardMissions.option_true
) or (
world.options.exclude_very_hard_missions == ExcludeVeryHardMissions.option_default
and (
mission_order_type not in [MissionOrder.option_vanilla_shuffled, MissionOrder.option_grid]
or (
mission_order_type == MissionOrder.option_grid
and world.options.maximum_campaign_size < 20
)
)
if world.options.exclude_very_hard_missions == ExcludeVeryHardMissions.option_true or (
world.options.exclude_very_hard_missions == ExcludeVeryHardMissions.option_default and (
mission_order_type in dynamic_mission_orders and world.options.maximum_campaign_size < 20 or
mission_order_type == MissionOrder.option_mini_campaign
)
):
excluded_missions = excluded_missions.union(
[mission for mission in SC2Mission if
Expand All @@ -959,6 +951,22 @@ def get_excluded_missions(world: 'SC2World') -> Set[SC2Mission]:
MissionOrder.option_mini_campaign
]

static_mission_orders = {
MissionOrder.option_vanilla: vanilla_shuffle_order,
MissionOrder.option_vanilla_shuffled: vanilla_shuffle_order,
MissionOrder.option_mini_campaign: mini_campaign_order
}

dynamic_mission_orders = [
MissionOrder.option_golden_path,
MissionOrder.option_grid,
MissionOrder.option_gauntlet,
MissionOrder.option_blitz,
MissionOrder.option_hopscotch
]

LEGACY_GRID_ORDERS = {3, 4, 8} # Medium Grid, Mini Grid, and Tiny Grid respectively

kerrigan_unit_available = [
KerriganPresence.option_vanilla,
]
1 change: 1 addition & 0 deletions worlds/sc2/PoolFilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
get_enabled_campaigns, RequiredTactics, kerrigan_unit_available, GrantStoryTech,
TakeOverAIAllies, campaign_depending_orders,
ShuffleCampaigns, get_excluded_missions, ShuffleNoBuild, ExtraLocations, GrantStoryLevels,
static_mission_orders, dynamic_mission_orders
)
from . import ItemNames, ItemGroups

Expand Down
41 changes: 34 additions & 7 deletions worlds/sc2/Regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from BaseClasses import Region, Entrance, Location, CollectionState
from .Locations import LocationData
from .Options import get_option_value, MissionOrder, get_enabled_campaigns, campaign_depending_orders, \
GridTwoStartPositions
from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, \
MissionPools, SC2Campaign, get_goal_location, SC2Mission, MissionConnection
GridTwoStartPositions, static_mission_orders, dynamic_mission_orders
from .MissionTables import MissionInfo, vanilla_mission_req_table, \
MissionPools, SC2Campaign, get_goal_location, SC2Mission, MissionConnection, FillMission
from .MissionOrders import make_gauntlet, make_blitz, make_golden_path, make_hopscotch
from .PoolFilter import filter_missions


Expand All @@ -29,7 +30,7 @@ def create_regions(
* int The number of missions in the world
* str The name of the goal location
"""
mission_order_type: int = get_option_value(world, "mission_order")
mission_order_type: MissionOrder = world.options.mission_order

if mission_order_type == MissionOrder.option_vanilla:
return create_vanilla_regions(world, locations, location_cache)
Expand Down Expand Up @@ -379,15 +380,39 @@ def make_grid_connect_rule(
return lambda state: state.has(f"Beat {missions[connected_coords].mission_name}", player)


def make_dynamic_mission_order(
world: 'SC2World',
mission_order_type: int
) -> Dict[SC2Campaign, List[FillMission]]:
mission_pools = filter_missions(world)

mission_pool = [mission for mission_pool in mission_pools.values() for mission in mission_pool]

num_missions = min(len(mission_pool), get_option_value(world, "maximum_campaign_size"))
num_missions = max(2, num_missions)
if mission_order_type == MissionOrder.option_golden_path:
return make_golden_path(world, num_missions)
# Grid handled by dedicated region generator
elif mission_order_type == MissionOrder.option_gauntlet:
return make_gauntlet(num_missions)
elif mission_order_type == MissionOrder.option_blitz:
return make_blitz(num_missions)
elif mission_order_type == MissionOrder.option_hopscotch:
return make_hopscotch(world.options.grid_two_start_positions, num_missions)


def create_structured_regions(
world: 'SC2World',
locations: Tuple[LocationData, ...],
location_cache: List[Location],
mission_order_type: int,
mission_order_type: MissionOrder,
) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
locations_per_region = get_locations_per_region(locations)

mission_order = mission_orders[mission_order_type]()
if mission_order_type in static_mission_orders:
mission_order = static_mission_orders[mission_order_type]()
else:
mission_order = make_dynamic_mission_order(world, mission_order_type)
enabled_campaigns = get_enabled_campaigns(world)
shuffle_campaigns = get_option_value(world, "shuffle_campaigns")

Expand Down Expand Up @@ -596,7 +621,9 @@ def build_connection_rule(mission_names: List[str], missions_req: int) -> Callab
mission.slot, connections, mission_order[campaign][i].category,
number=mission_order[campaign][i].number,
completion_critical=mission_order[campaign][i].completion_critical,
or_requirements=mission_order[campaign][i].or_requirements)})
or_requirements=mission_order[campaign][i].or_requirements,
ui_vertical_padding=mission_order[campaign][i].ui_vertical_padding),
})

final_mission_id = final_mission.id
# Changing the completion condition for alternate final missions into an event
Expand Down
5 changes: 2 additions & 3 deletions worlds/sc2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def fill_slot_data(self):
slot_data["nova_covert_ops_only"] = (enabled_campaigns == {SC2Campaign.NCO})
slot_data["mission_req"] = slot_req_table
slot_data["final_mission"] = self.final_mission_id
slot_data["version"] = 3
slot_data["version"] = 4

if SC2Campaign.HOTS not in enabled_campaigns:
slot_data["kerrigan_presence"] = KerriganPresence.option_not_present
Expand Down Expand Up @@ -450,8 +450,7 @@ def flag_start_unit(world: SC2World, item_list: List[FilterItem], starter_unit:
# If the first mission is a logic-less no-build
missions = get_all_missions(world.mission_req_table)
build_missions = [mission for mission in missions if MissionFlag.NoBuild not in mission.flags]
races = set(mission.race for mission in build_missions)
races.remove(SC2Race.ANY)
races = {mission.race for mission in build_missions if mission.race != SC2Race.ANY}
if races:
first_race = world.random.choice(list(races))

Expand Down

0 comments on commit f0325e7

Please sign in to comment.