2022-10-01 13:24:05 +00:00
|
|
|
from typing import List, Set, Dict, Tuple, Optional, Callable
|
2023-02-14 00:06:43 +00:00
|
|
|
from BaseClasses import MultiWorld, Region, Entrance, Location
|
2022-05-18 21:27:38 +00:00
|
|
|
from .Locations import LocationData
|
2022-05-26 17:28:10 +00:00
|
|
|
from .Options import get_option_value
|
2022-10-26 10:24:54 +00:00
|
|
|
from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations
|
|
|
|
from .PoolFilter import filter_missions
|
2022-05-18 21:27:38 +00:00
|
|
|
|
|
|
|
|
2022-12-11 19:46:24 +00:00
|
|
|
def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\
|
2022-10-26 10:24:54 +00:00
|
|
|
-> Tuple[Dict[str, MissionInfo], int, str]:
|
2022-05-18 21:27:38 +00:00
|
|
|
locations_per_region = get_locations_per_region(locations)
|
|
|
|
|
2022-12-11 19:46:24 +00:00
|
|
|
mission_order_type = get_option_value(multiworld, player, "mission_order")
|
2022-10-26 10:24:54 +00:00
|
|
|
mission_order = mission_orders[mission_order_type]
|
|
|
|
|
2022-12-11 19:46:24 +00:00
|
|
|
mission_pools = filter_missions(multiworld, player)
|
2022-10-26 10:24:54 +00:00
|
|
|
final_mission = mission_pools['all_in'][0]
|
|
|
|
|
|
|
|
used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
|
2022-12-11 19:46:24 +00:00
|
|
|
regions = [create_region(multiworld, player, locations_per_region, location_cache, "Menu")]
|
2022-10-26 10:24:54 +00:00
|
|
|
for region_name in used_regions:
|
2022-12-11 19:46:24 +00:00
|
|
|
regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name))
|
2022-10-26 10:24:54 +00:00
|
|
|
# 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'
|
2022-05-18 21:27:38 +00:00
|
|
|
|
|
|
|
if __debug__:
|
2022-10-26 10:24:54 +00:00
|
|
|
if mission_order_type in (0, 1):
|
|
|
|
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
|
2022-05-18 21:27:38 +00:00
|
|
|
|
2022-12-11 19:46:24 +00:00
|
|
|
multiworld.regions += regions
|
2022-05-18 21:27:38 +00:00
|
|
|
|
|
|
|
names: Dict[str, int] = {}
|
|
|
|
|
2022-10-26 10:24:54 +00:00
|
|
|
if mission_order_type == 0:
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Menu', 'Liberation Day'),
|
|
|
|
connect(multiworld, player, names, 'Liberation Day', 'The Outlaws',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Liberation Day", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'The Outlaws', 'Zero Hour',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat The Outlaws", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Zero Hour', 'Evacuation',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Zero Hour", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Evacuation', 'Outbreak',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Evacuation", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, "Outbreak", "Safe Haven",
|
|
|
|
lambda state: state._sc2wol_cleared_missions(multiworld, player, 7) and
|
2022-05-26 17:28:10 +00:00
|
|
|
state.has("Beat Outbreak", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, "Outbreak", "Haven's Fall",
|
|
|
|
lambda state: state._sc2wol_cleared_missions(multiworld, player, 7) and
|
2022-05-26 17:28:10 +00:00
|
|
|
state.has("Beat Outbreak", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Zero Hour', 'Smash and Grab',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Zero Hour", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Smash and Grab', 'The Dig',
|
|
|
|
lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
|
2022-05-26 17:28:10 +00:00
|
|
|
state.has("Beat Smash and Grab", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'The Dig', 'The Moebius Factor',
|
|
|
|
lambda state: state._sc2wol_cleared_missions(multiworld, player, 11) and
|
2022-05-26 17:28:10 +00:00
|
|
|
state.has("Beat The Dig", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'The Moebius Factor', 'Supernova',
|
|
|
|
lambda state: state._sc2wol_cleared_missions(multiworld, player, 14) and
|
2022-05-26 17:28:10 +00:00
|
|
|
state.has("Beat The Moebius Factor", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Supernova', 'Maw of the Void',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Supernova", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Zero Hour', "Devil's Playground",
|
|
|
|
lambda state: state._sc2wol_cleared_missions(multiworld, player, 4) and
|
2022-05-26 17:28:10 +00:00
|
|
|
state.has("Beat Zero Hour", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, "Devil's Playground", 'Welcome to the Jungle',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Devil's Playground", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, "Welcome to the Jungle", 'Breakout',
|
|
|
|
lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
|
2022-05-26 17:28:10 +00:00
|
|
|
state.has("Beat Welcome to the Jungle", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, "Welcome to the Jungle", 'Ghost of a Chance',
|
|
|
|
lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
|
2022-05-26 17:28:10 +00:00
|
|
|
state.has("Beat Welcome to the Jungle", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, "Zero Hour", 'The Great Train Robbery',
|
|
|
|
lambda state: state._sc2wol_cleared_missions(multiworld, player, 6) and
|
2022-05-26 17:28:10 +00:00
|
|
|
state.has("Beat Zero Hour", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'The Great Train Robbery', 'Cutthroat',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat The Great Train Robbery", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Cutthroat', 'Engine of Destruction',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Cutthroat", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Engine of Destruction', 'Media Blitz',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Engine of Destruction", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Media Blitz', 'Piercing the Shroud',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Media Blitz", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'The Dig', 'Whispers of Doom',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat The Dig", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Whispers of Doom', 'A Sinister Turn',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Whispers of Doom", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'A Sinister Turn', 'Echoes of the Future',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat A Sinister Turn", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Echoes of the Future', 'In Utter Darkness',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Echoes of the Future", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Maw of the Void', 'Gates of Hell',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Maw of the Void", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Gates of Hell', 'Belly of the Beast',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Gates of Hell", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Gates of Hell', 'Shatter the Sky',
|
2022-05-26 17:28:10 +00:00
|
|
|
lambda state: state.has("Beat Gates of Hell", player)),
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, 'Gates of Hell', 'All-In',
|
2022-05-26 17:28:10 +00:00
|
|
|
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)))
|
|
|
|
|
2022-10-26 10:24:54 +00:00
|
|
|
return vanilla_mission_req_table, 29, final_location
|
2022-05-26 17:28:10 +00:00
|
|
|
|
2022-10-26 10:24:54 +00:00
|
|
|
else:
|
2022-05-26 17:28:10 +00:00
|
|
|
missions = []
|
|
|
|
|
|
|
|
# Initial fill out of mission list and marking all-in mission
|
2022-10-26 10:24:54 +00:00
|
|
|
for mission in mission_order:
|
|
|
|
if mission is None:
|
|
|
|
missions.append(None)
|
|
|
|
elif mission.type == "all_in":
|
|
|
|
missions.append(final_mission)
|
2022-12-11 19:46:24 +00:00
|
|
|
elif mission.relegate and not get_option_value(multiworld, player, "shuffle_no_build"):
|
2022-06-03 18:18:36 +00:00
|
|
|
missions.append("no_build")
|
2022-05-26 17:28:10 +00:00
|
|
|
else:
|
|
|
|
missions.append(mission.type)
|
|
|
|
|
2022-10-26 10:24:54 +00:00
|
|
|
# Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled
|
2022-12-11 19:46:24 +00:00
|
|
|
if get_option_value(multiworld, player, "shuffle_protoss") == 0 and mission_order_type == 1:
|
2022-05-26 17:28:10 +00:00
|
|
|
missions[22] = "A Sinister Turn"
|
2022-10-26 10:24:54 +00:00
|
|
|
mission_pools['medium'].remove("A Sinister Turn")
|
2022-05-26 17:28:10 +00:00
|
|
|
missions[23] = "Echoes of the Future"
|
2022-10-26 10:24:54 +00:00
|
|
|
mission_pools['medium'].remove("Echoes of the Future")
|
2022-05-26 17:28:10 +00:00
|
|
|
missions[24] = "In Utter Darkness"
|
2022-10-26 10:24:54 +00:00
|
|
|
mission_pools['hard'].remove("In Utter Darkness")
|
2022-05-26 17:28:10 +00:00
|
|
|
|
|
|
|
no_build_slots = []
|
|
|
|
easy_slots = []
|
|
|
|
medium_slots = []
|
|
|
|
hard_slots = []
|
|
|
|
|
|
|
|
# Search through missions to find slots needed to fill
|
|
|
|
for i in range(len(missions)):
|
2022-10-26 10:24:54 +00:00
|
|
|
if missions[i] is None:
|
|
|
|
continue
|
2022-05-26 17:28:10 +00:00
|
|
|
if missions[i] == "no_build":
|
|
|
|
no_build_slots.append(i)
|
|
|
|
elif missions[i] == "easy":
|
|
|
|
easy_slots.append(i)
|
|
|
|
elif missions[i] == "medium":
|
|
|
|
medium_slots.append(i)
|
|
|
|
elif missions[i] == "hard":
|
|
|
|
hard_slots.append(i)
|
|
|
|
|
|
|
|
# Add no_build missions to the pool and fill in no_build slots
|
2022-10-26 10:24:54 +00:00
|
|
|
missions_to_add = mission_pools['no_build']
|
2022-05-26 17:28:10 +00:00
|
|
|
for slot in no_build_slots:
|
2022-12-11 19:46:24 +00:00
|
|
|
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
|
2022-05-26 17:28:10 +00:00
|
|
|
|
|
|
|
missions[slot] = missions_to_add.pop(filler)
|
|
|
|
|
|
|
|
# Add easy missions into pool and fill in easy slots
|
2022-10-26 10:24:54 +00:00
|
|
|
missions_to_add = missions_to_add + mission_pools['easy']
|
2022-05-26 17:28:10 +00:00
|
|
|
for slot in easy_slots:
|
2022-12-11 19:46:24 +00:00
|
|
|
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
|
2022-05-26 17:28:10 +00:00
|
|
|
|
|
|
|
missions[slot] = missions_to_add.pop(filler)
|
|
|
|
|
|
|
|
# Add medium missions into pool and fill in medium slots
|
2022-10-26 10:24:54 +00:00
|
|
|
missions_to_add = missions_to_add + mission_pools['medium']
|
2022-05-26 17:28:10 +00:00
|
|
|
for slot in medium_slots:
|
2022-12-11 19:46:24 +00:00
|
|
|
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
|
2022-05-26 17:28:10 +00:00
|
|
|
|
|
|
|
missions[slot] = missions_to_add.pop(filler)
|
|
|
|
|
|
|
|
# Add hard missions into pool and fill in hard slots
|
2022-10-26 10:24:54 +00:00
|
|
|
missions_to_add = missions_to_add + mission_pools['hard']
|
2022-05-26 17:28:10 +00:00
|
|
|
for slot in hard_slots:
|
2022-12-11 19:46:24 +00:00
|
|
|
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
|
2022-05-26 17:28:10 +00:00
|
|
|
|
|
|
|
missions[slot] = missions_to_add.pop(filler)
|
|
|
|
|
|
|
|
# Loop through missions to create requirements table and connect regions
|
|
|
|
# TODO: Handle 'and' connections
|
|
|
|
mission_req_table = {}
|
|
|
|
for i in range(len(missions)):
|
|
|
|
connections = []
|
2022-10-26 10:24:54 +00:00
|
|
|
for connection in mission_order[i].connect_to:
|
2022-05-26 17:28:10 +00:00
|
|
|
if connection == -1:
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, "Menu", missions[i])
|
2022-05-26 17:28:10 +00:00
|
|
|
else:
|
2022-12-11 19:46:24 +00:00
|
|
|
connect(multiworld, player, names, missions[connection], missions[i],
|
2022-08-19 20:50:44 +00:00
|
|
|
(lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and
|
2022-12-11 19:46:24 +00:00
|
|
|
state._sc2wol_cleared_missions(multiworld, player,
|
2022-08-19 20:50:44 +00:00
|
|
|
missions_req)))
|
2022-10-26 10:24:54 +00:00
|
|
|
(missions[connection], mission_order[i].number))
|
2022-05-26 17:28:10 +00:00
|
|
|
connections.append(connection + 1)
|
|
|
|
|
|
|
|
mission_req_table.update({missions[i]: MissionInfo(
|
2022-10-26 10:24:54 +00:00
|
|
|
vanilla_mission_req_table[missions[i]].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)})
|
2022-05-26 17:28:10 +00:00
|
|
|
|
2022-10-26 10:24:54 +00:00
|
|
|
final_mission_id = vanilla_mission_req_table[final_mission].id
|
|
|
|
return mission_req_table, final_mission_id, final_mission + ': Victory'
|
2022-05-18 21:27:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]):
|
|
|
|
existingRegions = set()
|
|
|
|
|
|
|
|
for region in regions:
|
|
|
|
existingRegions.add(region.name)
|
|
|
|
|
|
|
|
if (regionNames - existingRegions):
|
2022-05-21 00:47:16 +00:00
|
|
|
raise Exception("Starcraft: the following regions are used in locations: {}, but no such region exists".format(
|
|
|
|
regionNames - existingRegions))
|
2022-05-18 21:27:38 +00:00
|
|
|
|
|
|
|
|
2022-05-21 00:47:16 +00:00
|
|
|
def create_location(player: int, location_data: LocationData, region: Region,
|
|
|
|
location_cache: List[Location]) -> Location:
|
2022-05-18 21:27:38 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2022-12-11 19:46:24 +00:00
|
|
|
def create_region(multiworld: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]],
|
2022-05-21 00:47:16 +00:00
|
|
|
location_cache: List[Location], name: str) -> Region:
|
2023-02-14 00:06:43 +00:00
|
|
|
region = Region(name, player, multiworld)
|
2022-05-18 21:27:38 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2022-05-21 00:47:16 +00:00
|
|
|
def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str,
|
|
|
|
rule: Optional[Callable] = None):
|
2022-05-18 21:27:38 +00:00
|
|
|
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]]:
|
2022-05-21 00:47:16 +00:00
|
|
|
per_region: Dict[str, List[LocationData]] = {}
|
2022-05-18 21:27:38 +00:00
|
|
|
|
|
|
|
for location in locations:
|
|
|
|
per_region.setdefault(location.region, []).append(location)
|
|
|
|
|
|
|
|
return per_region
|