Files
Grinch-AP/worlds/sc2wol/Regions.py
Magnemania 17e90ce12c SC2: Greater variety on short generations (#1367)
Originally, short generations used an artificial cull to create balanced mission distributions. This resulted in campaigns that were somewhat too consistent, and on some standard settings combinations, this resulted in campaigns having The Outlaws as the second mission 100% of the time. It also caused generation to fail a bit too easily if the player excluded too many missions.

This removes the cull and adds an additional early Easy mission slot to all of the reduced sized campaigns.

When playing on No Build settings, this also pushes many of the missions down a difficulty level to ensure greater variety, and pushes additional missions down on Advanced Tactics.

Additional small fixes:

The in-world Excluded Missions validation check is replaced by the core OptionSet check.
Fixed issue with Existing Items not getting their upgrades locked with Units Always Have Upgrades on.
2023-03-07 14:14:49 +01:00

289 lines
14 KiB
Python

from typing import List, Set, Dict, Tuple, Optional, Callable
from BaseClasses import MultiWorld, Region, Entrance, Location
from .Locations import LocationData
from .Options import get_option_value
from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, MissionPools
from .PoolFilter import filter_missions
def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\
-> Tuple[Dict[str, MissionInfo], int, str]:
locations_per_region = get_locations_per_region(locations)
mission_order_type = get_option_value(multiworld, player, "mission_order")
mission_order = mission_orders[mission_order_type]
mission_pools = filter_missions(multiworld, player)
regions = [create_region(multiworld, player, locations_per_region, location_cache, "Menu")]
names: Dict[str, int] = {}
if mission_order_type == 0:
# Generating all regions and locations
for region_name in vanilla_mission_req_table.keys():
regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name))
multiworld.regions += regions
connect(multiworld, player, names, 'Menu', 'Liberation Day'),
connect(multiworld, player, names, 'Liberation Day', 'The Outlaws',
lambda state: state.has("Beat Liberation Day", player)),
connect(multiworld, player, names, 'The Outlaws', 'Zero Hour',
lambda state: state.has("Beat The Outlaws", player)),
connect(multiworld, player, names, 'Zero Hour', 'Evacuation',
lambda state: state.has("Beat Zero Hour", player)),
connect(multiworld, player, names, 'Evacuation', 'Outbreak',
lambda state: state.has("Beat Evacuation", player)),
connect(multiworld, player, names, "Outbreak", "Safe Haven",
lambda state: state._sc2wol_cleared_missions(multiworld, player, 7) and
state.has("Beat Outbreak", player)),
connect(multiworld, player, names, "Outbreak", "Haven's Fall",
lambda state: state._sc2wol_cleared_missions(multiworld, player, 7) and
state.has("Beat Outbreak", player)),
connect(multiworld, player, names, 'Zero Hour', 'Smash and Grab',
lambda state: state.has("Beat Zero Hour", player)),
connect(multiworld, player, names, 'Smash and Grab', 'The Dig',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
state.has("Beat Smash and Grab", player)),
connect(multiworld, player, names, 'The Dig', 'The Moebius Factor',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 11) and
state.has("Beat The Dig", player)),
connect(multiworld, player, names, 'The Moebius Factor', 'Supernova',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 14) and
state.has("Beat The Moebius Factor", player)),
connect(multiworld, player, names, 'Supernova', 'Maw of the Void',
lambda state: state.has("Beat Supernova", player)),
connect(multiworld, player, names, 'Zero Hour', "Devil's Playground",
lambda state: state._sc2wol_cleared_missions(multiworld, player, 4) and
state.has("Beat Zero Hour", player)),
connect(multiworld, player, names, "Devil's Playground", 'Welcome to the Jungle',
lambda state: state.has("Beat Devil's Playground", player)),
connect(multiworld, player, names, "Welcome to the Jungle", 'Breakout',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
state.has("Beat Welcome to the Jungle", player)),
connect(multiworld, player, names, "Welcome to the Jungle", 'Ghost of a Chance',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
state.has("Beat Welcome to the Jungle", player)),
connect(multiworld, player, names, "Zero Hour", 'The Great Train Robbery',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 6) and
state.has("Beat Zero Hour", player)),
connect(multiworld, player, names, 'The Great Train Robbery', 'Cutthroat',
lambda state: state.has("Beat The Great Train Robbery", player)),
connect(multiworld, player, names, 'Cutthroat', 'Engine of Destruction',
lambda state: state.has("Beat Cutthroat", player)),
connect(multiworld, player, names, 'Engine of Destruction', 'Media Blitz',
lambda state: state.has("Beat Engine of Destruction", player)),
connect(multiworld, player, names, 'Media Blitz', 'Piercing the Shroud',
lambda state: state.has("Beat Media Blitz", player)),
connect(multiworld, player, names, 'The Dig', 'Whispers of Doom',
lambda state: state.has("Beat The Dig", player)),
connect(multiworld, player, names, 'Whispers of Doom', 'A Sinister Turn',
lambda state: state.has("Beat Whispers of Doom", player)),
connect(multiworld, player, names, 'A Sinister Turn', 'Echoes of the Future',
lambda state: state.has("Beat A Sinister Turn", player)),
connect(multiworld, player, names, 'Echoes of the Future', 'In Utter Darkness',
lambda state: state.has("Beat Echoes of the Future", player)),
connect(multiworld, player, names, 'Maw of the Void', 'Gates of Hell',
lambda state: state.has("Beat Maw of the Void", player)),
connect(multiworld, player, names, 'Gates of Hell', 'Belly of the Beast',
lambda state: state.has("Beat Gates of Hell", player)),
connect(multiworld, player, names, 'Gates of Hell', 'Shatter the Sky',
lambda state: state.has("Beat Gates of Hell", player)),
connect(multiworld, player, names, 'Gates of Hell', 'All-In',
lambda state: state.has('Beat Gates of Hell', player) and (
state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player)))
return vanilla_mission_req_table, 29, 'All-In: Victory'
else:
missions = []
remove_prophecy = mission_order_type == 1 and not get_option_value(multiworld, player, "shuffle_protoss")
final_mission = mission_pools[MissionPools.FINAL][0]
# Determining if missions must be removed
mission_pool_size = sum(len(mission_pool) for mission_pool in mission_pools.values())
removals = len(mission_order) - mission_pool_size
# Removing entire Prophecy chain on vanilla shuffled when not shuffling protoss
if remove_prophecy:
removals -= 4
# Initial fill out of mission list and marking all-in mission
for mission in mission_order:
# Removing extra missions if mission pool is too small
if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy:
missions.append(None)
elif mission.type == MissionPools.FINAL:
missions.append(final_mission)
else:
missions.append(mission.type)
no_build_slots = []
easy_slots = []
medium_slots = []
hard_slots = []
# Search through missions to find slots needed to fill
for i in range(len(missions)):
if missions[i] is None:
continue
if missions[i] == MissionPools.STARTER:
no_build_slots.append(i)
elif missions[i] == MissionPools.EASY:
easy_slots.append(i)
elif missions[i] == MissionPools.MEDIUM:
medium_slots.append(i)
elif missions[i] == MissionPools.HARD:
hard_slots.append(i)
# Add no_build missions to the pool and fill in no_build slots
missions_to_add = mission_pools[MissionPools.STARTER]
if len(no_build_slots) > len(missions_to_add):
raise Exception("There are no valid No-Build missions. Please exclude fewer missions.")
for slot in no_build_slots:
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Add easy missions into pool and fill in easy slots
missions_to_add = missions_to_add + mission_pools[MissionPools.EASY]
if len(easy_slots) > len(missions_to_add):
raise Exception("There are not enough Easy missions to fill the campaign. Please exclude fewer missions.")
for slot in easy_slots:
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Add medium missions into pool and fill in medium slots
missions_to_add = missions_to_add + mission_pools[MissionPools.MEDIUM]
if len(medium_slots) > len(missions_to_add):
raise Exception("There are not enough Easy and Medium missions to fill the campaign. Please exclude fewer missions.")
for slot in medium_slots:
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Add hard missions into pool and fill in hard slots
missions_to_add = missions_to_add + mission_pools[MissionPools.HARD]
if len(hard_slots) > len(missions_to_add):
raise Exception("There are not enough missions to fill the campaign. Please exclude fewer missions.")
for slot in hard_slots:
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Generating regions and locations from selected missions
for region_name in missions:
regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name))
multiworld.regions += regions
# Mapping original mission slots to shifted mission slots when missions are removed
slot_map = []
slot_offset = 0
for position, mission in enumerate(missions):
slot_map.append(position - slot_offset + 1)
if mission is None:
slot_offset += 1
# Loop through missions to create requirements table and connect regions
# TODO: Handle 'and' connections
mission_req_table = {}
for i, mission in enumerate(missions):
if mission is None:
continue
connections = []
for connection in mission_order[i].connect_to:
required_mission = missions[connection]
if connection == -1:
connect(multiworld, player, names, "Menu", mission)
elif required_mission is None:
continue
else:
connect(multiworld, player, names, required_mission, mission,
(lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and
state._sc2wol_cleared_missions(multiworld, player,
missions_req)))
(missions[connection], mission_order[i].number))
connections.append(slot_map[connection])
mission_req_table.update({mission: MissionInfo(
vanilla_mission_req_table[mission].id, connections, mission_order[i].category,
number=mission_order[i].number,
completion_critical=mission_order[i].completion_critical,
or_requirements=mission_order[i].or_requirements)})
final_mission_id = vanilla_mission_req_table[final_mission].id
# Changing the completion condition for alternate final missions into an event
if final_mission != 'All-In':
final_location = alt_final_mission_locations[final_mission]
# Final location should be near the end of the cache
for i in range(len(location_cache) - 1, -1, -1):
if location_cache[i].name == final_location:
location_cache[i].locked = True
location_cache[i].event = True
location_cache[i].address = None
break
else:
final_location = 'All-In: Victory'
return mission_req_table, final_mission_id, final_location
def create_location(player: int, location_data: LocationData, region: Region,
location_cache: List[Location]) -> Location:
location = Location(player, location_data.name, location_data.code, region)
location.access_rule = location_data.rule
if id is None:
location.event = True
location.locked = True
location_cache.append(location)
return location
def create_region(multiworld: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]],
location_cache: List[Location], name: str) -> Region:
region = Region(name, player, multiworld)
if name in locations_per_region:
for location_data in locations_per_region[name]:
location = create_location(player, location_data, region, location_cache)
region.locations.append(location)
return region
def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str,
rule: Optional[Callable] = None):
sourceRegion = world.get_region(source, player)
targetRegion = world.get_region(target, player)
if target not in used_names:
used_names[target] = 1
name = target
else:
used_names[target] += 1
name = target + (' ' * used_names[target])
connection = Entrance(player, name, sourceRegion)
if rule:
connection.access_rule = rule
sourceRegion.exits.append(connection)
connection.connect(targetRegion)
def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]:
per_region: Dict[str, List[LocationData]] = {}
for location in locations:
per_region.setdefault(location.region, []).append(location)
return per_region