forked from mirror/Archipelago
SC2: Options to curate random item pool and control early unit placement
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user