SC2: Options to curate random item pool and control early unit placement

This commit is contained in:
Magnemania
2022-10-05 21:46:08 -04:00
parent 7f8fce5a51
commit 58f392a0af
3 changed files with 87 additions and 39 deletions

View File

@@ -1,6 +1,6 @@
from typing import Dict
from BaseClasses import MultiWorld
from Options import Choice, Option, DefaultOnToggle
from Options import Choice, Option, DefaultOnToggle, ItemSet
class GameDifficulty(Choice):
@@ -39,20 +39,26 @@ class MissionOrder(Choice):
"""Determines the order the missions are played in.
Vanilla: Keeps the standard mission order and branching from the WoL Campaign.
Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.
Mini Shuffle: Halves the number of missions in the campaign and randomizes the missions available.
Gauntlet: A linear series of 7 random missions to complete the campaign.
Mini Shuffle: Shorter version of the campaign with randomized missions and optional branches.
Grid: A 4x4 grid of random missions. Start at the top left and forge a path towards All-In.
Mini Grid: A 3x3 version of Grid.
Blitz: 10 random missions that open up very quickly.
Gauntlet: Linear series of 7 random missions to complete the campaign.
"""
display_name = "Mission Order"
option_vanilla = 0
option_vanilla_shuffled = 1
option_mini_shuffle = 2
option_gauntlet = 3
option_grid = 3
option_mini_grid = 4
option_blitz = 5
option_gauntlet = 6
class ShuffleProtoss(DefaultOnToggle):
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled.
On Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain if not shuffled.
On Mini Shuffle or Gauntlet, the 3 protoss missions will not appear and Protoss units are removed from the pool if not shuffled.
On reduced mission settings, the 3 protoss missions will not appear and Protoss units are removed from the pool if not shuffled.
"""
display_name = "Shuffle Protoss Missions"
@@ -60,10 +66,25 @@ class ShuffleProtoss(DefaultOnToggle):
class RelegateNoBuildMissions(DefaultOnToggle):
"""Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
On Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be placed at the end of optional routes.
On Mini Shuffle or Gauntlet, the 5 no-build missions will not appear."""
On reduced mission settings, the 5 no-build missions will not appear."""
display_name = "Relegate No-Build Missions"
class EarlyUnit(DefaultOnToggle):
"""Guarantees that the first mission will contain a unit."""
display_name = "Early Unit"
class LockedItems(ItemSet):
"""Guarantees that these items will appear in your world"""
display_name = "Locked Items"
class ExcludedItems(ItemSet):
"""Guarantees that these items will not appear in your world"""
display_name = "Excluded Items"
# noinspection PyTypeChecker
sc2wol_options: Dict[str, Option] = {
"game_difficulty": GameDifficulty,
@@ -72,7 +93,10 @@ sc2wol_options: Dict[str, Option] = {
"all_in_map": AllInMap,
"mission_order": MissionOrder,
"shuffle_protoss": ShuffleProtoss,
"relegate_no_build": RelegateNoBuildMissions
"relegate_no_build": RelegateNoBuildMissions,
"early_unit": EarlyUnit,
"locked_items": LockedItems,
"excluded_items": ExcludedItems
}

View File

@@ -40,8 +40,14 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]:
shuffle_protoss = get_option_value(world, player, "shuffle_protoss")
relegate_no_build = get_option_value(world, player, "relegate_no_build")
mission_count = 0
for mission in mission_orders[mission_order_type]:
if mission.type == 'all_in': # All-In is placed separately
continue
mission_count += 1
# Vanilla and Vanilla Shuffled use the entire mission pool
if mission_order_type in (0, 1):
if mission_count == 28:
return {
'no_build': no_build_regions_list[:],
'easy': easy_regions_list[:],
@@ -49,14 +55,6 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]:
'hard': hard_regions_list[:]
}
mission_count = 0
for mission in mission_orders[mission_order_type]:
if mission is None:
continue
if mission.type == 'all_in': # All-In is placed separately
continue
mission_count += 1
mission_sets = [
set(no_build_regions_list),
set(easy_regions_list),
@@ -99,6 +97,14 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]:
}
def filter_upgrades(inventory: list[Item], parent_item: Item or str):
item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
return [
inv_item for inv_item in inventory
if inv_item.name in ALWAYS_USEFUL_ARMORY or not inv_item.name.endswith('(' + item_name + ')')
]
class ValidInventory:
def has(self, item: str, player: int):
@@ -121,12 +127,14 @@ class ValidInventory:
self.logical_inventory = {item.name for item in self.progression_items.union(self.locked_items)}
requirements = mission_requirements
mission_order_type = get_option_value(self.world, self.player, "mission_order")
min_units_per_structure = MIN_UNITS_PER_STRUCTURE[mission_order_type]
# Inventory restrictiveness based on number of missions with checks
mission_count = len(mission_orders[mission_order_type]) - 1
min_units_per_structure = int(mission_count / 7)
if min_units_per_structure > 0:
requirements.append(lambda state: state.has_units_per_structure(min_units_per_structure))
while len(inventory) + len(locked_items) > inventory_size:
if len(inventory) == 0:
raise Exception('Reduced item pool generation failed.')
raise Exception('Reduced item pool generation failed - not enough locations available to place items.')
# Select random item from removable items
item = self.world.random.choice(inventory)
inventory.remove(item)
@@ -137,10 +145,7 @@ class ValidInventory:
# If item can be removed and is a unit, remove armory upgrades
# Some armory upgrades are kept regardless, as they remain logically relevant
if item.name in UPGRADABLE_ITEMS:
inventory = [
inv_item for inv_item in inventory
if inv_item.name in ALWAYS_USEFUL_ARMORY or not inv_item.name.endswith('(' + item.name + ')')
]
inventory = filter_upgrades(inventory, item)
else:
# If item cannot be removed, move it to locked items
self.logical_inventory.add(item.name)
@@ -162,7 +167,8 @@ class ValidInventory:
self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player)
self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player)
self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player)
self._sc2wol_has_bunker_unit = lambda world, player: SC2WoLLogic._sc2wol_has_bunker_unit(self, world, player)
self._sc2wol_has_manned_bunkers = lambda world, player: SC2WoLLogic._sc2wol_has_manned_bunkers(self, world, player)
self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player)
def __init__(self, world: MultiWorld, player: int,
item_pool: list[Item], existing_items: list[Item], locked_items: list[Item],

View File

@@ -9,7 +9,7 @@ from .Locations import get_locations
from .Regions import create_regions
from .Options import sc2wol_options, get_option_value
from .LogicMixin import SC2WoLLogic
from .PoolFilter import filter_missions, filter_items
from .PoolFilter import filter_missions, filter_items, filter_upgrades
from .MissionTables import starting_mission_locations, MissionInfo
@@ -122,26 +122,32 @@ def get_excluded_items(self: SC2WoLWorld, world: MultiWorld, player: int) -> Set
for item in world.precollected_items[player]:
excluded_items.add(item.name)
excluded_items_option = getattr(world, 'excluded_items', [])
excluded_items.update(excluded_items_option[player].value)
return excluded_items
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]) -> List[Item]:
non_local_items = world.non_local_items[player].value
if get_option_value(world, player, "early_unit"):
local_basic_unit = tuple(item for item in basic_unit if item not in non_local_items)
if not local_basic_unit:
raise Exception("At least one basic unit must be local")
local_basic_unit = tuple(item for item in basic_unit if item not in non_local_items)
if not local_basic_unit:
raise Exception("At least one basic unit must be local")
# The first world should also be the starting world
first_mission = list(world.worlds[player].mission_req_table)[0]
if first_mission in starting_mission_locations:
first_location = starting_mission_locations[first_mission]
elif first_mission == "In Utter Darkness":
first_location = first_mission + ": Defeat"
else:
first_location = first_mission + ": Victory"
# The first world should also be the starting world
first_mission = list(world.worlds[player].mission_req_table)[0]
if first_mission in starting_mission_locations:
first_location = starting_mission_locations[first_mission]
elif first_mission == "In Utter Darkness":
first_location = first_mission + ": Defeat"
return [assign_starter_item(world, player, excluded_items, locked_locations, first_location, local_basic_unit)]
else:
first_location = first_mission + ": Victory"
return [assign_starter_item(world, player, excluded_items, locked_locations, first_location, local_basic_unit)]
return []
def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
@@ -164,15 +170,27 @@ def get_item_pool(world: MultiWorld, player: int, mission_req_table: dict[str, M
starter_items: List[str], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
pool: List[Item] = []
# For the future: goal items like Artifact Shards go here
locked_items = []
# YAML items
locked_items_option = getattr(world, 'locked_items', [])
yaml_locked_items = locked_items_option[player].value
for name, data in item_table.items():
if name not in excluded_items:
for _ in range(data.quantity):
item = create_item_with_correct_settings(world, player, name)
pool.append(item)
if name in yaml_locked_items:
locked_items.append(item)
else:
pool.append(item)
existing_items = starter_items + [item.name for item in world.precollected_items[player]]
# For the future: goal items like Artifact Shards go here
locked_items = []
# Removing upgrades for excluded items
for item_name in excluded_items:
pool = filter_upgrades(pool, item_name)
filtered_pool = filter_items(world, player, mission_req_table, location_cache, pool, existing_items, locked_items)
return filtered_pool