SC2: Content update (#5312)

Feature highlights:
- Adds many content to the SC2 game
- Allows custom mission order
- Adds race-swapped missions for build missions (except Epilogue and NCO)
- Allows War Council Nerfs (Protoss units can get pre - War Council State, alternative units get another custom nerf to match the power level of base units)
- Revamps Predator's upgrade tree (never was considered strategically important)
- Adds some units and upgrades
- Locked and excluded items can specify quantity
- Key mode (if opt-in, missions require keys to be unlocked on top of their regular regular requirements
- Victory caches - Victory locations can grant multiple items to the multiworld instead of one 
- The generator is more resilient for generator failures as it validates logic for item excludes
- Fixes the following issues:
  - https://github.com/ArchipelagoMW/Archipelago/issues/3531 
  - https://github.com/ArchipelagoMW/Archipelago/issues/3548
This commit is contained in:
Ziktofel
2025-09-02 17:40:58 +02:00
committed by GitHub
parent 2359cceb64
commit 5f1835c546
73 changed files with 46368 additions and 13655 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,306 +0,0 @@
from typing import *
import asyncio
from NetUtils import JSONMessagePart
from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.gridlayout import GridLayout
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivymd.uix.tooltip import MDTooltip
from kivy.uix.scrollview import ScrollView
from kivy.properties import StringProperty
from .Client import SC2Context, calc_unfinished_missions, parse_unlock
from .MissionTables import (lookup_id_to_mission, lookup_name_to_mission, campaign_race_exceptions, SC2Mission, SC2Race,
SC2Campaign)
from .Locations import LocationType, lookup_location_id_to_type
from .Options import LocationInclusion
from . import SC2World, get_first_mission
class HoverableButton(HoverBehavior, Button):
pass
class MissionButton(HoverableButton, MDTooltip):
tooltip_text = StringProperty("Test")
def __init__(self, *args, **kwargs):
super(HoverableButton, self).__init__(**kwargs)
self._tooltip = ServerToolTip(text=self.text, markup=True)
self._tooltip.padding = [5, 2, 5, 2]
def on_enter(self):
self._tooltip.text = self.tooltip_text
if self.tooltip_text != "":
self.display_tooltip()
def on_leave(self):
self.remove_tooltip()
@property
def ctx(self) -> SC2Context:
return App.get_running_app().ctx
class CampaignScroll(ScrollView):
pass
class MultiCampaignLayout(GridLayout):
pass
class CampaignLayout(GridLayout):
pass
class MissionLayout(GridLayout):
pass
class MissionCategory(GridLayout):
pass
class SC2JSONtoKivyParser(KivyJSONtoTextParser):
def _handle_text(self, node: JSONMessagePart):
if node.get("keep_markup", False):
for ref in node.get("refs", []):
node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]"
self.ref_count += 1
return super(KivyJSONtoTextParser, self)._handle_text(node)
else:
return super()._handle_text(node)
class SC2Manager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("Starcraft2", "Starcraft2"),
]
base_title = "Archipelago Starcraft 2 Client"
campaign_panel: Optional[CampaignLayout] = None
last_checked_locations: Set[int] = set()
mission_id_to_button: Dict[int, MissionButton] = {}
launching: Union[bool, int] = False # if int -> mission ID
refresh_from_launching = True
first_check = True
first_mission = ""
ctx: SC2Context
def __init__(self, ctx) -> None:
super().__init__(ctx)
self.json_to_kivy_parser = SC2JSONtoKivyParser(ctx)
def clear_tooltip(self) -> None:
if self.ctx.current_tooltip:
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
self.ctx.current_tooltip = None
def build(self):
container = super().build()
panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll())
self.campaign_panel = MultiCampaignLayout()
panel.content.add_widget(self.campaign_panel)
Clock.schedule_interval(self.build_mission_table, 0.5)
return container
def build_mission_table(self, dt) -> None:
if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
not self.refresh_from_launching)) or self.first_check:
assert self.campaign_panel is not None
self.refresh_from_launching = True
self.campaign_panel.clear_widgets()
if self.ctx.mission_req_table:
self.last_checked_locations = self.ctx.checked_locations.copy()
self.first_check = False
self.first_mission = get_first_mission(self.ctx.mission_req_table)
self.mission_id_to_button = {}
available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
multi_campaign_layout_height = 0
for campaign, missions in sorted(self.ctx.mission_req_table.items(), key=lambda item: item[0].id):
categories: Dict[str, List[str]] = {}
# separate missions into categories
for mission_index in missions:
mission_info = self.ctx.mission_req_table[campaign][mission_index]
if mission_info.category not in categories:
categories[mission_info.category] = []
categories[mission_info.category].append(mission_index)
max_mission_count = max(len(categories[category]) for category in categories)
if max_mission_count == 1:
campaign_layout_height = 115
else:
campaign_layout_height = (max_mission_count + 2) * 50
multi_campaign_layout_height += campaign_layout_height
campaign_layout = CampaignLayout(size_hint_y=None, height=campaign_layout_height)
if campaign != SC2Campaign.GLOBAL:
campaign_layout.add_widget(
Label(text=campaign.campaign_name, size_hint_y=None, height=25, outline_width=1)
)
mission_layout = MissionLayout()
for category in categories:
category_name_height = 0
category_spacing = 3
if category.startswith('_'):
category_display_name = ''
else:
category_display_name = category
category_name_height += 25
category_spacing = 10
category_panel = MissionCategory(padding=[category_spacing,6,category_spacing,6])
category_panel.add_widget(
Label(text=category_display_name, size_hint_y=None, height=category_name_height, outline_width=1))
for mission in categories[category]:
text: str = mission
tooltip: str = ""
mission_obj: SC2Mission = lookup_name_to_mission[mission]
mission_id: int = mission_obj.id
mission_data = self.ctx.mission_req_table[campaign][mission]
remaining_locations, plando_locations, remaining_count = self.sort_unfinished_locations(mission)
# Map has uncollected locations
if mission in unfinished_missions:
if self.any_valuable_locations(remaining_locations):
text = f"[color=6495ED]{text}[/color]"
else:
text = f"[color=A0BEF4]{text}[/color]"
elif mission in available_missions:
text = f"[color=FFFFFF]{text}[/color]"
# Map requirements not met
else:
text = f"[color=a9a9a9]{text}[/color]"
tooltip = f"Requires: "
if mission_data.required_world:
tooltip += ", ".join(list(self.ctx.mission_req_table[parse_unlock(req_mission).campaign])[parse_unlock(req_mission).connect_to - 1] for
req_mission in
mission_data.required_world)
if mission_data.number:
tooltip += " and "
if mission_data.number:
tooltip += f"{self.ctx.mission_req_table[campaign][mission].number} missions completed"
if mission_id == self.ctx.final_mission:
if mission in available_missions:
text = f"[color=FFBC95]{mission}[/color]"
else:
text = f"[color=D0C0BE]{mission}[/color]"
if tooltip:
tooltip += "\n"
tooltip += "Final Mission"
if remaining_count > 0:
if tooltip:
tooltip += "\n\n"
tooltip += f"-- Uncollected locations --"
for loctype in LocationType:
if len(remaining_locations[loctype]) > 0:
if loctype == LocationType.VICTORY:
tooltip += f"\n- {remaining_locations[loctype][0]}"
else:
tooltip += f"\n{self.get_location_type_title(loctype)}:\n- "
tooltip += "\n- ".join(remaining_locations[loctype])
if len(plando_locations) > 0:
tooltip += f"\nPlando:\n- "
tooltip += "\n- ".join(plando_locations)
MISSION_BUTTON_HEIGHT = 50
for pad in range(mission_data.ui_vertical_padding):
column_spacer = Label(text='', size_hint_y=None, height=MISSION_BUTTON_HEIGHT)
category_panel.add_widget(column_spacer)
mission_button = MissionButton(text=text, size_hint_y=None, height=MISSION_BUTTON_HEIGHT)
mission_race = mission_obj.race
if mission_race == SC2Race.ANY:
mission_race = mission_obj.campaign.race
race = campaign_race_exceptions.get(mission_obj, mission_race)
racial_colors = {
SC2Race.TERRAN: (0.24, 0.84, 0.68),
SC2Race.ZERG: (1, 0.65, 0.37),
SC2Race.PROTOSS: (0.55, 0.7, 1)
}
if race in racial_colors:
mission_button.background_color = racial_colors[race]
mission_button.tooltip_text = tooltip
mission_button.bind(on_press=self.mission_callback)
self.mission_id_to_button[mission_id] = mission_button
category_panel.add_widget(mission_button)
category_panel.add_widget(Label(text=""))
mission_layout.add_widget(category_panel)
campaign_layout.add_widget(mission_layout)
self.campaign_panel.add_widget(campaign_layout)
self.campaign_panel.height = multi_campaign_layout_height
elif self.launching:
assert self.campaign_panel is not None
self.refresh_from_launching = False
self.campaign_panel.clear_widgets()
self.campaign_panel.add_widget(Label(text="Launching Mission: " +
lookup_id_to_mission[self.launching].mission_name))
if self.ctx.ui:
self.ctx.ui.clear_tooltip()
def mission_callback(self, button: MissionButton) -> None:
if not self.launching:
mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button)
if self.ctx.play_mission(mission_id):
self.launching = mission_id
Clock.schedule_once(self.finish_launching, 10)
def finish_launching(self, dt):
self.launching = False
def sort_unfinished_locations(self, mission_name: str) -> Tuple[Dict[LocationType, List[str]], List[str], int]:
locations: Dict[LocationType, List[str]] = {loctype: [] for loctype in LocationType}
count = 0
for loc in self.ctx.locations_for_mission(mission_name):
if loc in self.ctx.missing_locations:
count += 1
locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names.lookup_in_game(loc))
plando_locations = []
for plando_loc in self.ctx.plando_locations:
for loctype in LocationType:
if plando_loc in locations[loctype]:
locations[loctype].remove(plando_loc)
plando_locations.append(plando_loc)
return locations, plando_locations, count
def any_valuable_locations(self, locations: Dict[LocationType, List[str]]) -> bool:
for loctype in LocationType:
if len(locations[loctype]) > 0 and self.ctx.location_inclusions[loctype] == LocationInclusion.option_enabled:
return True
return False
def get_location_type_title(self, location_type: LocationType) -> str:
title = location_type.name.title().replace("_", " ")
if self.ctx.location_inclusions[location_type] == LocationInclusion.option_disabled:
title += " (Nothing)"
elif self.ctx.location_inclusions[location_type] == LocationInclusion.option_resources:
title += " (Resources)"
else:
title += ""
return title
def start_gui(context: SC2Context):
context.ui = SC2Manager(context)
context.ui_task = asyncio.create_task(context.ui.async_run(), name="UI")
import pkgutil
data = pkgutil.get_data(SC2World.__module__, "Starcraft2.kv").decode()
Builder.load_string(data)

View File

@@ -1,100 +0,0 @@
import typing
from . import Items, ItemNames
from .MissionTables import campaign_mission_table, SC2Campaign, SC2Mission
"""
Item name groups, given to Archipelago and used in YAMLs and /received filtering.
For non-developers the following will be useful:
* Items with a bracket get groups named after the unbracketed part
* eg. "Advanced Healing AI (Medivac)" is accessible as "Advanced Healing AI"
* The exception to this are item names that would be ambiguous (eg. "Resource Efficiency")
* Item flaggroups get unique groups as well as combined groups for numbered flaggroups
* eg. "Unit" contains all units, "Armory" contains "Armory 1" through "Armory 6"
* The best place to look these up is at the bottom of Items.py
* Items that have a parent are grouped together
* eg. "Zergling Items" contains all items that have "Zergling" as a parent
* These groups do NOT contain the parent item
* This currently does not include items with multiple potential parents, like some LotV unit upgrades
* All items are grouped by their race ("Terran", "Protoss", "Zerg", "Any")
* Hand-crafted item groups can be found at the bottom of this file
"""
item_name_groups: typing.Dict[str, typing.List[str]] = {}
# Groups for use in world logic
item_name_groups["Missions"] = ["Beat " + mission.mission_name for mission in SC2Mission]
item_name_groups["WoL Missions"] = ["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.WOL]] + \
["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.PROPHECY]]
# These item name groups should not show up in documentation
unlisted_item_name_groups = {
"Missions", "WoL Missions"
}
# Some item names only differ in bracketed parts
# These items are ambiguous for short-hand name groups
bracketless_duplicates: typing.Set[str]
# This is a list of names in ItemNames with bracketed parts removed, for internal use
_shortened_names = [(name[:name.find(' (')] if '(' in name else name)
for name in [ItemNames.__dict__[name] for name in ItemNames.__dir__() if not name.startswith('_')]]
# Remove the first instance of every short-name from the full item list
bracketless_duplicates = set(_shortened_names)
for name in bracketless_duplicates:
_shortened_names.remove(name)
# The remaining short-names are the duplicates
bracketless_duplicates = set(_shortened_names)
del _shortened_names
# All items get sorted into their data type
for item, data in Items.get_full_item_list().items():
# Items get assigned to their flaggroup's type
item_name_groups.setdefault(data.type, []).append(item)
# Numbered flaggroups get sorted into an unnumbered group
# Currently supports numbers of one or two digits
if data.type[-2:].strip().isnumeric():
type_group = data.type[:-2].strip()
item_name_groups.setdefault(type_group, []).append(item)
# Flaggroups with numbers are unlisted
unlisted_item_name_groups.add(data.type)
# Items with a bracket get a short-hand name group for ease of use in YAMLs
if '(' in item:
short_name = item[:item.find(' (')]
# Ambiguous short-names are dropped
if short_name not in bracketless_duplicates:
item_name_groups[short_name] = [item]
# Short-name groups are unlisted
unlisted_item_name_groups.add(short_name)
# Items with a parent get assigned to their parent's group
if data.parent_item:
# The parent groups need a special name, otherwise they are ambiguous with the parent
parent_group = f"{data.parent_item} Items"
item_name_groups.setdefault(parent_group, []).append(item)
# Parent groups are unlisted
unlisted_item_name_groups.add(parent_group)
# All items get assigned to their race's group
race_group = data.race.name.capitalize()
item_name_groups.setdefault(race_group, []).append(item)
# Hand-made groups
item_name_groups["Aiur"] = [
ItemNames.ZEALOT, ItemNames.DRAGOON, ItemNames.SENTRY, ItemNames.AVENGER, ItemNames.HIGH_TEMPLAR,
ItemNames.IMMORTAL, ItemNames.REAVER,
ItemNames.PHOENIX, ItemNames.SCOUT, ItemNames.ARBITER, ItemNames.CARRIER,
]
item_name_groups["Nerazim"] = [
ItemNames.CENTURION, ItemNames.STALKER, ItemNames.DARK_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.DARK_ARCHON,
ItemNames.ANNIHILATOR,
ItemNames.CORSAIR, ItemNames.ORACLE, ItemNames.VOID_RAY,
]
item_name_groups["Tal'Darim"] = [
ItemNames.SUPPLICANT, ItemNames.SLAYER, ItemNames.HAVOC, ItemNames.BLOOD_HUNTER, ItemNames.ASCENDANT,
ItemNames.VANGUARD, ItemNames.WRATHWALKER,
ItemNames.DESTROYER, ItemNames.MOTHERSHIP,
ItemNames.WARP_PRISM_PHASE_BLASTER,
]
item_name_groups["Purifier"] = [
ItemNames.SENTINEL, ItemNames.ADEPT, ItemNames.INSTIGATOR, ItemNames.ENERGIZER,
ItemNames.COLOSSUS, ItemNames.DISRUPTOR,
ItemNames.MIRAGE, ItemNames.TEMPEST,
]

View File

@@ -1,661 +0,0 @@
"""
A complete collection of Starcraft 2 item names as strings.
Users of this data may make some assumptions about the structure of a name:
* The upgrade for a unit will end with the unit's name in parentheses
* Weapon / armor upgrades may be grouped by a common prefix specified within this file
"""
# Terran Units
MARINE = "Marine"
MEDIC = "Medic"
FIREBAT = "Firebat"
MARAUDER = "Marauder"
REAPER = "Reaper"
HELLION = "Hellion"
VULTURE = "Vulture"
GOLIATH = "Goliath"
DIAMONDBACK = "Diamondback"
SIEGE_TANK = "Siege Tank"
MEDIVAC = "Medivac"
WRAITH = "Wraith"
VIKING = "Viking"
BANSHEE = "Banshee"
BATTLECRUISER = "Battlecruiser"
GHOST = "Ghost"
SPECTRE = "Spectre"
THOR = "Thor"
RAVEN = "Raven"
SCIENCE_VESSEL = "Science Vessel"
PREDATOR = "Predator"
HERCULES = "Hercules"
# Extended units
LIBERATOR = "Liberator"
VALKYRIE = "Valkyrie"
WIDOW_MINE = "Widow Mine"
CYCLONE = "Cyclone"
HERC = "HERC"
WARHOUND = "Warhound"
# Terran Buildings
BUNKER = "Bunker"
MISSILE_TURRET = "Missile Turret"
SENSOR_TOWER = "Sensor Tower"
PLANETARY_FORTRESS = "Planetary Fortress"
PERDITION_TURRET = "Perdition Turret"
HIVE_MIND_EMULATOR = "Hive Mind Emulator"
PSI_DISRUPTER = "Psi Disrupter"
# Terran Weapon / Armor Upgrades
TERRAN_UPGRADE_PREFIX = "Progressive Terran"
TERRAN_INFANTRY_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Infantry"
TERRAN_VEHICLE_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Vehicle"
TERRAN_SHIP_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Ship"
PROGRESSIVE_TERRAN_INFANTRY_WEAPON = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_TERRAN_INFANTRY_ARMOR = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Armor"
PROGRESSIVE_TERRAN_VEHICLE_WEAPON = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_TERRAN_VEHICLE_ARMOR = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Armor"
PROGRESSIVE_TERRAN_SHIP_WEAPON = f"{TERRAN_SHIP_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_TERRAN_SHIP_ARMOR = f"{TERRAN_SHIP_UPGRADE_PREFIX} Armor"
PROGRESSIVE_TERRAN_WEAPON_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Weapon Upgrade"
PROGRESSIVE_TERRAN_ARMOR_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Armor Upgrade"
PROGRESSIVE_TERRAN_INFANTRY_UPGRADE = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_TERRAN_VEHICLE_UPGRADE = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_TERRAN_SHIP_UPGRADE = f"{TERRAN_SHIP_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Weapon/Armor Upgrade"
# Mercenaries
WAR_PIGS = "War Pigs"
DEVIL_DOGS = "Devil Dogs"
HAMMER_SECURITIES = "Hammer Securities"
SPARTAN_COMPANY = "Spartan Company"
SIEGE_BREAKERS = "Siege Breakers"
HELS_ANGELS = "Hel's Angels"
DUSK_WINGS = "Dusk Wings"
JACKSONS_REVENGE = "Jackson's Revenge"
SKIBIS_ANGELS = "Skibi's Angels"
DEATH_HEADS = "Death Heads"
WINGED_NIGHTMARES = "Winged Nightmares"
MIDNIGHT_RIDERS = "Midnight Riders"
BRYNHILDS = "Brynhilds"
JOTUN = "Jotun"
# Lab / Global
ULTRA_CAPACITORS = "Ultra-Capacitors"
VANADIUM_PLATING = "Vanadium Plating"
ORBITAL_DEPOTS = "Orbital Depots"
MICRO_FILTERING = "Micro-Filtering"
AUTOMATED_REFINERY = "Automated Refinery"
COMMAND_CENTER_REACTOR = "Command Center Reactor"
TECH_REACTOR = "Tech Reactor"
ORBITAL_STRIKE = "Orbital Strike"
CELLULAR_REACTOR = "Cellular Reactor"
PROGRESSIVE_REGENERATIVE_BIO_STEEL = "Progressive Regenerative Bio-Steel"
PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM = "Progressive Fire-Suppression System"
PROGRESSIVE_ORBITAL_COMMAND = "Progressive Orbital Command"
STRUCTURE_ARMOR = "Structure Armor"
HI_SEC_AUTO_TRACKING = "Hi-Sec Auto Tracking"
ADVANCED_OPTICS = "Advanced Optics"
ROGUE_FORCES = "Rogue Forces"
# Terran Unit Upgrades
BANSHEE_HYPERFLIGHT_ROTORS = "Hyperflight Rotors (Banshee)"
BANSHEE_INTERNAL_TECH_MODULE = "Internal Tech Module (Banshee)"
BANSHEE_LASER_TARGETING_SYSTEM = "Laser Targeting System (Banshee)"
BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS = "Progressive Cross-Spectrum Dampeners (Banshee)"
BANSHEE_SHOCKWAVE_MISSILE_BATTERY = "Shockwave Missile Battery (Banshee)"
BANSHEE_SHAPED_HULL = "Shaped Hull (Banshee)"
BANSHEE_ADVANCED_TARGETING_OPTICS = "Advanced Targeting Optics (Banshee)"
BANSHEE_DISTORTION_BLASTERS = "Distortion Blasters (Banshee)"
BANSHEE_ROCKET_BARRAGE = "Rocket Barrage (Banshee)"
BATTLECRUISER_ATX_LASER_BATTERY = "ATX Laser Battery (Battlecruiser)"
BATTLECRUISER_CLOAK = "Cloak (Battlecruiser)"
BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX = "Progressive Defensive Matrix (Battlecruiser)"
BATTLECRUISER_INTERNAL_TECH_MODULE = "Internal Tech Module (Battlecruiser)"
BATTLECRUISER_PROGRESSIVE_MISSILE_PODS = "Progressive Missile Pods (Battlecruiser)"
BATTLECRUISER_OPTIMIZED_LOGISTICS = "Optimized Logistics (Battlecruiser)"
BATTLECRUISER_TACTICAL_JUMP = "Tactical Jump (Battlecruiser)"
BATTLECRUISER_BEHEMOTH_PLATING = "Behemoth Plating (Battlecruiser)"
BATTLECRUISER_COVERT_OPS_ENGINES = "Covert Ops Engines (Battlecruiser)"
BUNKER_NEOSTEEL_BUNKER = "Neosteel Bunker (Bunker)"
BUNKER_PROJECTILE_ACCELERATOR = "Projectile Accelerator (Bunker)"
BUNKER_SHRIKE_TURRET = "Shrike Turret (Bunker)"
BUNKER_FORTIFIED_BUNKER = "Fortified Bunker (Bunker)"
CYCLONE_MAG_FIELD_ACCELERATORS = "Mag-Field Accelerators (Cyclone)"
CYCLONE_MAG_FIELD_LAUNCHERS = "Mag-Field Launchers (Cyclone)"
CYCLONE_RAPID_FIRE_LAUNCHERS = "Rapid Fire Launchers (Cyclone)"
CYCLONE_TARGETING_OPTICS = "Targeting Optics (Cyclone)"
CYCLONE_RESOURCE_EFFICIENCY = "Resource Efficiency (Cyclone)"
CYCLONE_INTERNAL_TECH_MODULE = "Internal Tech Module (Cyclone)"
DIAMONDBACK_BURST_CAPACITORS = "Burst Capacitors (Diamondback)"
DIAMONDBACK_HYPERFLUXOR = "Hyperfluxor (Diamondback)"
DIAMONDBACK_RESOURCE_EFFICIENCY = "Resource Efficiency (Diamondback)"
DIAMONDBACK_SHAPED_HULL = "Shaped Hull (Diamondback)"
DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL = "Progressive Tri-Lithium Power Cell (Diamondback)"
DIAMONDBACK_ION_THRUSTERS = "Ion Thrusters (Diamondback)"
FIREBAT_INCINERATOR_GAUNTLETS = "Incinerator Gauntlets (Firebat)"
FIREBAT_JUGGERNAUT_PLATING = "Juggernaut Plating (Firebat)"
FIREBAT_RESOURCE_EFFICIENCY = "Resource Efficiency (Firebat)"
FIREBAT_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Firebat)"
FIREBAT_INFERNAL_PRE_IGNITER = "Infernal Pre-Igniter (Firebat)"
FIREBAT_KINETIC_FOAM = "Kinetic Foam (Firebat)"
FIREBAT_NANO_PROJECTORS = "Nano Projectors (Firebat)"
GHOST_CRIUS_SUIT = "Crius Suit (Ghost)"
GHOST_EMP_ROUNDS = "EMP Rounds (Ghost)"
GHOST_LOCKDOWN = "Lockdown (Ghost)"
GHOST_OCULAR_IMPLANTS = "Ocular Implants (Ghost)"
GHOST_RESOURCE_EFFICIENCY = "Resource Efficiency (Ghost)"
GOLIATH_ARES_CLASS_TARGETING_SYSTEM = "Ares-Class Targeting System (Goliath)"
GOLIATH_JUMP_JETS = "Jump Jets (Goliath)"
GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM = "Multi-Lock Weapons System (Goliath)"
GOLIATH_OPTIMIZED_LOGISTICS = "Optimized Logistics (Goliath)"
GOLIATH_SHAPED_HULL = "Shaped Hull (Goliath)"
GOLIATH_RESOURCE_EFFICIENCY = "Resource Efficiency (Goliath)"
GOLIATH_INTERNAL_TECH_MODULE = "Internal Tech Module (Goliath)"
HELLION_HELLBAT_ASPECT = "Hellbat Aspect (Hellion)"
HELLION_JUMP_JETS = "Jump Jets (Hellion)"
HELLION_OPTIMIZED_LOGISTICS = "Optimized Logistics (Hellion)"
HELLION_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Hellion)"
HELLION_SMART_SERVOS = "Smart Servos (Hellion)"
HELLION_THERMITE_FILAMENTS = "Thermite Filaments (Hellion)"
HELLION_TWIN_LINKED_FLAMETHROWER = "Twin-Linked Flamethrower (Hellion)"
HELLION_INFERNAL_PLATING = "Infernal Plating (Hellion)"
HERC_JUGGERNAUT_PLATING = "Juggernaut Plating (HERC)"
HERC_KINETIC_FOAM = "Kinetic Foam (HERC)"
HERC_RESOURCE_EFFICIENCY = "Resource Efficiency (HERC)"
HERCULES_INTERNAL_FUSION_MODULE = "Internal Fusion Module (Hercules)"
HERCULES_TACTICAL_JUMP = "Tactical Jump (Hercules)"
LIBERATOR_ADVANCED_BALLISTICS = "Advanced Ballistics (Liberator)"
LIBERATOR_CLOAK = "Cloak (Liberator)"
LIBERATOR_LASER_TARGETING_SYSTEM = "Laser Targeting System (Liberator)"
LIBERATOR_OPTIMIZED_LOGISTICS = "Optimized Logistics (Liberator)"
LIBERATOR_RAID_ARTILLERY = "Raid Artillery (Liberator)"
LIBERATOR_SMART_SERVOS = "Smart Servos (Liberator)"
LIBERATOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Liberator)"
MARAUDER_CONCUSSIVE_SHELLS = "Concussive Shells (Marauder)"
MARAUDER_INTERNAL_TECH_MODULE = "Internal Tech Module (Marauder)"
MARAUDER_KINETIC_FOAM = "Kinetic Foam (Marauder)"
MARAUDER_LASER_TARGETING_SYSTEM = "Laser Targeting System (Marauder)"
MARAUDER_MAGRAIL_MUNITIONS = "Magrail Munitions (Marauder)"
MARAUDER_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Marauder)"
MARAUDER_JUGGERNAUT_PLATING = "Juggernaut Plating (Marauder)"
MARINE_COMBAT_SHIELD = "Combat Shield (Marine)"
MARINE_LASER_TARGETING_SYSTEM = "Laser Targeting System (Marine)"
MARINE_MAGRAIL_MUNITIONS = "Magrail Munitions (Marine)"
MARINE_OPTIMIZED_LOGISTICS = "Optimized Logistics (Marine)"
MARINE_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Marine)"
MEDIC_ADVANCED_MEDIC_FACILITIES = "Advanced Medic Facilities (Medic)"
MEDIC_OPTICAL_FLARE = "Optical Flare (Medic)"
MEDIC_RESOURCE_EFFICIENCY = "Resource Efficiency (Medic)"
MEDIC_RESTORATION = "Restoration (Medic)"
MEDIC_STABILIZER_MEDPACKS = "Stabilizer Medpacks (Medic)"
MEDIC_ADAPTIVE_MEDPACKS = "Adaptive Medpacks (Medic)"
MEDIC_NANO_PROJECTOR = "Nano Projector (Medic)"
MEDIVAC_ADVANCED_HEALING_AI = "Advanced Healing AI (Medivac)"
MEDIVAC_AFTERBURNERS = "Afterburners (Medivac)"
MEDIVAC_EXPANDED_HULL = "Expanded Hull (Medivac)"
MEDIVAC_RAPID_DEPLOYMENT_TUBE = "Rapid Deployment Tube (Medivac)"
MEDIVAC_SCATTER_VEIL = "Scatter Veil (Medivac)"
MEDIVAC_ADVANCED_CLOAKING_FIELD = "Advanced Cloaking Field (Medivac)"
MISSILE_TURRET_HELLSTORM_BATTERIES = "Hellstorm Batteries (Missile Turret)"
MISSILE_TURRET_TITANIUM_HOUSING = "Titanium Housing (Missile Turret)"
PLANETARY_FORTRESS_PROGRESSIVE_AUGMENTED_THRUSTERS = "Progressive Augmented Thrusters (Planetary Fortress)"
PLANETARY_FORTRESS_ADVANCED_TARGETING = "Advanced Targeting (Planetary Fortress)"
PREDATOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Predator)"
PREDATOR_CLOAK = "Cloak (Predator)"
PREDATOR_CHARGE = "Charge (Predator)"
PREDATOR_PREDATOR_S_FURY = "Predator's Fury (Predator)"
RAVEN_ANTI_ARMOR_MISSILE = "Anti-Armor Missile (Raven)"
RAVEN_BIO_MECHANICAL_REPAIR_DRONE = "Bio Mechanical Repair Drone (Raven)"
RAVEN_HUNTER_SEEKER_WEAPON = "Hunter-Seeker Weapon (Raven)"
RAVEN_INTERFERENCE_MATRIX = "Interference Matrix (Raven)"
RAVEN_INTERNAL_TECH_MODULE = "Internal Tech Module (Raven)"
RAVEN_RAILGUN_TURRET = "Railgun Turret (Raven)"
RAVEN_SPIDER_MINES = "Spider Mines (Raven)"
RAVEN_RESOURCE_EFFICIENCY = "Resource Efficiency (Raven)"
RAVEN_DURABLE_MATERIALS = "Durable Materials (Raven)"
REAPER_ADVANCED_CLOAKING_FIELD = "Advanced Cloaking Field (Reaper)"
REAPER_COMBAT_DRUGS = "Combat Drugs (Reaper)"
REAPER_G4_CLUSTERBOMB = "G-4 Clusterbomb (Reaper)"
REAPER_LASER_TARGETING_SYSTEM = "Laser Targeting System (Reaper)"
REAPER_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Reaper)"
REAPER_SPIDER_MINES = "Spider Mines (Reaper)"
REAPER_U238_ROUNDS = "U-238 Rounds (Reaper)"
REAPER_JET_PACK_OVERDRIVE = "Jet Pack Overdrive (Reaper)"
SCIENCE_VESSEL_DEFENSIVE_MATRIX = "Defensive Matrix (Science Vessel)"
SCIENCE_VESSEL_EMP_SHOCKWAVE = "EMP Shockwave (Science Vessel)"
SCIENCE_VESSEL_IMPROVED_NANO_REPAIR = "Improved Nano-Repair (Science Vessel)"
SCIENCE_VESSEL_ADVANCED_AI_SYSTEMS = "Advanced AI Systems (Science Vessel)"
SCV_ADVANCED_CONSTRUCTION = "Advanced Construction (SCV)"
SCV_DUAL_FUSION_WELDERS = "Dual-Fusion Welders (SCV)"
SCV_HOSTILE_ENVIRONMENT_ADAPTATION = "Hostile Environment Adaptation (SCV)"
SIEGE_TANK_ADVANCED_SIEGE_TECH = "Advanced Siege Tech (Siege Tank)"
SIEGE_TANK_GRADUATING_RANGE = "Graduating Range (Siege Tank)"
SIEGE_TANK_INTERNAL_TECH_MODULE = "Internal Tech Module (Siege Tank)"
SIEGE_TANK_JUMP_JETS = "Jump Jets (Siege Tank)"
SIEGE_TANK_LASER_TARGETING_SYSTEM = "Laser Targeting System (Siege Tank)"
SIEGE_TANK_MAELSTROM_ROUNDS = "Maelstrom Rounds (Siege Tank)"
SIEGE_TANK_SHAPED_BLAST = "Shaped Blast (Siege Tank)"
SIEGE_TANK_SMART_SERVOS = "Smart Servos (Siege Tank)"
SIEGE_TANK_SPIDER_MINES = "Spider Mines (Siege Tank)"
SIEGE_TANK_SHAPED_HULL = "Shaped Hull (Siege Tank)"
SIEGE_TANK_RESOURCE_EFFICIENCY = "Resource Efficiency (Siege Tank)"
SPECTRE_IMPALER_ROUNDS = "Impaler Rounds (Spectre)"
SPECTRE_NYX_CLASS_CLOAKING_MODULE = "Nyx-Class Cloaking Module (Spectre)"
SPECTRE_PSIONIC_LASH = "Psionic Lash (Spectre)"
SPECTRE_RESOURCE_EFFICIENCY = "Resource Efficiency (Spectre)"
SPIDER_MINE_CERBERUS_MINE = "Cerberus Mine (Spider Mine)"
SPIDER_MINE_HIGH_EXPLOSIVE_MUNITION = "High Explosive Munition (Spider Mine)"
THOR_330MM_BARRAGE_CANNON = "330mm Barrage Cannon (Thor)"
THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL = "Progressive Immortality Protocol (Thor)"
THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD = "Progressive High Impact Payload (Thor)"
THOR_BUTTON_WITH_A_SKULL_ON_IT = "Button With a Skull on It (Thor)"
THOR_LASER_TARGETING_SYSTEM = "Laser Targeting System (Thor)"
THOR_LARGE_SCALE_FIELD_CONSTRUCTION = "Large Scale Field Construction (Thor)"
VALKYRIE_AFTERBURNERS = "Afterburners (Valkyrie)"
VALKYRIE_FLECHETTE_MISSILES = "Flechette Missiles (Valkyrie)"
VALKYRIE_ENHANCED_CLUSTER_LAUNCHERS = "Enhanced Cluster Launchers (Valkyrie)"
VALKYRIE_SHAPED_HULL = "Shaped Hull (Valkyrie)"
VALKYRIE_LAUNCHING_VECTOR_COMPENSATOR = "Launching Vector Compensator (Valkyrie)"
VALKYRIE_RESOURCE_EFFICIENCY = "Resource Efficiency (Valkyrie)"
VIKING_ANTI_MECHANICAL_MUNITION = "Anti-Mechanical Munition (Viking)"
VIKING_PHOBOS_CLASS_WEAPONS_SYSTEM = "Phobos-Class Weapons System (Viking)"
VIKING_RIPWAVE_MISSILES = "Ripwave Missiles (Viking)"
VIKING_SMART_SERVOS = "Smart Servos (Viking)"
VIKING_SHREDDER_ROUNDS = "Shredder Rounds (Viking)"
VIKING_WILD_MISSILES = "W.I.L.D. Missiles (Viking)"
VULTURE_AUTO_LAUNCHERS = "Auto Launchers (Vulture)"
VULTURE_ION_THRUSTERS = "Ion Thrusters (Vulture)"
VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE = "Progressive Replenishable Magazine (Vulture)"
VULTURE_AUTO_REPAIR = "Auto-Repair (Vulture)"
WARHOUND_RESOURCE_EFFICIENCY = "Resource Efficiency (Warhound)"
WARHOUND_REINFORCED_PLATING = "Reinforced Plating (Warhound)"
WIDOW_MINE_BLACK_MARKET_LAUNCHERS = "Black Market Launchers (Widow Mine)"
WIDOW_MINE_CONCEALMENT = "Concealment (Widow Mine)"
WIDOW_MINE_DRILLING_CLAWS = "Drilling Claws (Widow Mine)"
WIDOW_MINE_EXECUTIONER_MISSILES = "Executioner Missiles (Widow Mine)"
WRAITH_ADVANCED_LASER_TECHNOLOGY = "Advanced Laser Technology (Wraith)"
WRAITH_DISPLACEMENT_FIELD = "Displacement Field (Wraith)"
WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS = "Progressive Tomahawk Power Cells (Wraith)"
WRAITH_TRIGGER_OVERRIDE = "Trigger Override (Wraith)"
WRAITH_INTERNAL_TECH_MODULE = "Internal Tech Module (Wraith)"
WRAITH_RESOURCE_EFFICIENCY = "Resource Efficiency (Wraith)"
# Nova
NOVA_GHOST_VISOR = "Ghost Visor (Nova Equipment)"
NOVA_RANGEFINDER_OCULUS = "Rangefinder Oculus (Nova Equipment)"
NOVA_DOMINATION = "Domination (Nova Ability)"
NOVA_BLINK = "Blink (Nova Ability)"
NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE = "Progressive Stealth Suit Module (Nova Suit Module)"
NOVA_ENERGY_SUIT_MODULE = "Energy Suit Module (Nova Suit Module)"
NOVA_ARMORED_SUIT_MODULE = "Armored Suit Module (Nova Suit Module)"
NOVA_JUMP_SUIT_MODULE = "Jump Suit Module (Nova Suit Module)"
NOVA_C20A_CANISTER_RIFLE = "C20A Canister Rifle (Nova Weapon)"
NOVA_HELLFIRE_SHOTGUN = "Hellfire Shotgun (Nova Weapon)"
NOVA_PLASMA_RIFLE = "Plasma Rifle (Nova Weapon)"
NOVA_MONOMOLECULAR_BLADE = "Monomolecular Blade (Nova Weapon)"
NOVA_BLAZEFIRE_GUNBLADE = "Blazefire Gunblade (Nova Weapon)"
NOVA_STIM_INFUSION = "Stim Infusion (Nova Gadget)"
NOVA_PULSE_GRENADES = "Pulse Grenades (Nova Gadget)"
NOVA_FLASHBANG_GRENADES = "Flashbang Grenades (Nova Gadget)"
NOVA_IONIC_FORCE_FIELD = "Ionic Force Field (Nova Gadget)"
NOVA_HOLO_DECOY = "Holo Decoy (Nova Gadget)"
NOVA_NUKE = "Tac Nuke Strike (Nova Ability)"
# Zerg Units
ZERGLING = "Zergling"
SWARM_QUEEN = "Swarm Queen"
ROACH = "Roach"
HYDRALISK = "Hydralisk"
ABERRATION = "Aberration"
MUTALISK = "Mutalisk"
SWARM_HOST = "Swarm Host"
INFESTOR = "Infestor"
ULTRALISK = "Ultralisk"
CORRUPTOR = "Corruptor"
SCOURGE = "Scourge"
BROOD_QUEEN = "Brood Queen"
DEFILER = "Defiler"
# Zerg Buildings
SPORE_CRAWLER = "Spore Crawler"
SPINE_CRAWLER = "Spine Crawler"
# Zerg Weapon / Armor Upgrades
ZERG_UPGRADE_PREFIX = "Progressive Zerg"
ZERG_FLYER_UPGRADE_PREFIX = f"{ZERG_UPGRADE_PREFIX} Flyer"
PROGRESSIVE_ZERG_MELEE_ATTACK = f"{ZERG_UPGRADE_PREFIX} Melee Attack"
PROGRESSIVE_ZERG_MISSILE_ATTACK = f"{ZERG_UPGRADE_PREFIX} Missile Attack"
PROGRESSIVE_ZERG_GROUND_CARAPACE = f"{ZERG_UPGRADE_PREFIX} Ground Carapace"
PROGRESSIVE_ZERG_FLYER_ATTACK = f"{ZERG_FLYER_UPGRADE_PREFIX} Attack"
PROGRESSIVE_ZERG_FLYER_CARAPACE = f"{ZERG_FLYER_UPGRADE_PREFIX} Carapace"
PROGRESSIVE_ZERG_WEAPON_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Weapon Upgrade"
PROGRESSIVE_ZERG_ARMOR_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Armor Upgrade"
PROGRESSIVE_ZERG_GROUND_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Ground Upgrade"
PROGRESSIVE_ZERG_FLYER_UPGRADE = f"{ZERG_FLYER_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Weapon/Armor Upgrade"
# Zerg Unit Upgrades
ZERGLING_HARDENED_CARAPACE = "Hardened Carapace (Zergling)"
ZERGLING_ADRENAL_OVERLOAD = "Adrenal Overload (Zergling)"
ZERGLING_METABOLIC_BOOST = "Metabolic Boost (Zergling)"
ZERGLING_SHREDDING_CLAWS = "Shredding Claws (Zergling)"
ROACH_HYDRIODIC_BILE = "Hydriodic Bile (Roach)"
ROACH_ADAPTIVE_PLATING = "Adaptive Plating (Roach)"
ROACH_TUNNELING_CLAWS = "Tunneling Claws (Roach)"
ROACH_GLIAL_RECONSTITUTION = "Glial Reconstitution (Roach)"
ROACH_ORGANIC_CARAPACE = "Organic Carapace (Roach)"
HYDRALISK_FRENZY = "Frenzy (Hydralisk)"
HYDRALISK_ANCILLARY_CARAPACE = "Ancillary Carapace (Hydralisk)"
HYDRALISK_GROOVED_SPINES = "Grooved Spines (Hydralisk)"
HYDRALISK_MUSCULAR_AUGMENTS = "Muscular Augments (Hydralisk)"
HYDRALISK_RESOURCE_EFFICIENCY = "Resource Efficiency (Hydralisk)"
BANELING_CORROSIVE_ACID = "Corrosive Acid (Baneling)"
BANELING_RUPTURE = "Rupture (Baneling)"
BANELING_REGENERATIVE_ACID = "Regenerative Acid (Baneling)"
BANELING_CENTRIFUGAL_HOOKS = "Centrifugal Hooks (Baneling)"
BANELING_TUNNELING_JAWS = "Tunneling Jaws (Baneling)"
BANELING_RAPID_METAMORPH = "Rapid Metamorph (Baneling)"
MUTALISK_VICIOUS_GLAIVE = "Vicious Glaive (Mutalisk)"
MUTALISK_RAPID_REGENERATION = "Rapid Regeneration (Mutalisk)"
MUTALISK_SUNDERING_GLAIVE = "Sundering Glaive (Mutalisk)"
MUTALISK_SEVERING_GLAIVE = "Severing Glaive (Mutalisk)"
MUTALISK_AERODYNAMIC_GLAIVE_SHAPE = "Aerodynamic Glaive Shape (Mutalisk)"
SWARM_HOST_BURROW = "Burrow (Swarm Host)"
SWARM_HOST_RAPID_INCUBATION = "Rapid Incubation (Swarm Host)"
SWARM_HOST_PRESSURIZED_GLANDS = "Pressurized Glands (Swarm Host)"
SWARM_HOST_LOCUST_METABOLIC_BOOST = "Locust Metabolic Boost (Swarm Host)"
SWARM_HOST_ENDURING_LOCUSTS = "Enduring Locusts (Swarm Host)"
SWARM_HOST_ORGANIC_CARAPACE = "Organic Carapace (Swarm Host)"
SWARM_HOST_RESOURCE_EFFICIENCY = "Resource Efficiency (Swarm Host)"
ULTRALISK_BURROW_CHARGE = "Burrow Charge (Ultralisk)"
ULTRALISK_TISSUE_ASSIMILATION = "Tissue Assimilation (Ultralisk)"
ULTRALISK_MONARCH_BLADES = "Monarch Blades (Ultralisk)"
ULTRALISK_ANABOLIC_SYNTHESIS = "Anabolic Synthesis (Ultralisk)"
ULTRALISK_CHITINOUS_PLATING = "Chitinous Plating (Ultralisk)"
ULTRALISK_ORGANIC_CARAPACE = "Organic Carapace (Ultralisk)"
ULTRALISK_RESOURCE_EFFICIENCY = "Resource Efficiency (Ultralisk)"
CORRUPTOR_CORRUPTION = "Corruption (Corruptor)"
CORRUPTOR_CAUSTIC_SPRAY = "Caustic Spray (Corruptor)"
SCOURGE_VIRULENT_SPORES = "Virulent Spores (Scourge)"
SCOURGE_RESOURCE_EFFICIENCY = "Resource Efficiency (Scourge)"
SCOURGE_SWARM_SCOURGE = "Swarm Scourge (Scourge)"
DEVOURER_CORROSIVE_SPRAY = "Corrosive Spray (Devourer)"
DEVOURER_GAPING_MAW = "Gaping Maw (Devourer)"
DEVOURER_IMPROVED_OSMOSIS = "Improved Osmosis (Devourer)"
DEVOURER_PRESCIENT_SPORES = "Prescient Spores (Devourer)"
GUARDIAN_PROLONGED_DISPERSION = "Prolonged Dispersion (Guardian)"
GUARDIAN_PRIMAL_ADAPTATION = "Primal Adaptation (Guardian)"
GUARDIAN_SORONAN_ACID = "Soronan Acid (Guardian)"
IMPALER_ADAPTIVE_TALONS = "Adaptive Talons (Impaler)"
IMPALER_SECRETION_GLANDS = "Secretion Glands (Impaler)"
IMPALER_HARDENED_TENTACLE_SPINES = "Hardened Tentacle Spines (Impaler)"
LURKER_SEISMIC_SPINES = "Seismic Spines (Lurker)"
LURKER_ADAPTED_SPINES = "Adapted Spines (Lurker)"
RAVAGER_POTENT_BILE = "Potent Bile (Ravager)"
RAVAGER_BLOATED_BILE_DUCTS = "Bloated Bile Ducts (Ravager)"
RAVAGER_DEEP_TUNNEL = "Deep Tunnel (Ravager)"
VIPER_PARASITIC_BOMB = "Parasitic Bomb (Viper)"
VIPER_PARALYTIC_BARBS = "Paralytic Barbs (Viper)"
VIPER_VIRULENT_MICROBES = "Virulent Microbes (Viper)"
BROOD_LORD_POROUS_CARTILAGE = "Porous Cartilage (Brood Lord)"
BROOD_LORD_EVOLVED_CARAPACE = "Evolved Carapace (Brood Lord)"
BROOD_LORD_SPLITTER_MITOSIS = "Splitter Mitosis (Brood Lord)"
BROOD_LORD_RESOURCE_EFFICIENCY = "Resource Efficiency (Brood Lord)"
INFESTOR_INFESTED_TERRAN = "Infested Terran (Infestor)"
INFESTOR_MICROBIAL_SHROUD = "Microbial Shroud (Infestor)"
SWARM_QUEEN_SPAWN_LARVAE = "Spawn Larvae (Swarm Queen)"
SWARM_QUEEN_DEEP_TUNNEL = "Deep Tunnel (Swarm Queen)"
SWARM_QUEEN_ORGANIC_CARAPACE = "Organic Carapace (Swarm Queen)"
SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION = "Bio-Mechanical Transfusion (Swarm Queen)"
SWARM_QUEEN_RESOURCE_EFFICIENCY = "Resource Efficiency (Swarm Queen)"
SWARM_QUEEN_INCUBATOR_CHAMBER = "Incubator Chamber (Swarm Queen)"
BROOD_QUEEN_FUNGAL_GROWTH = "Fungal Growth (Brood Queen)"
BROOD_QUEEN_ENSNARE = "Ensnare (Brood Queen)"
BROOD_QUEEN_ENHANCED_MITOCHONDRIA = "Enhanced Mitochondria (Brood Queen)"
# Zerg Strains
ZERGLING_RAPTOR_STRAIN = "Raptor Strain (Zergling)"
ZERGLING_SWARMLING_STRAIN = "Swarmling Strain (Zergling)"
ROACH_VILE_STRAIN = "Vile Strain (Roach)"
ROACH_CORPSER_STRAIN = "Corpser Strain (Roach)"
BANELING_SPLITTER_STRAIN = "Splitter Strain (Baneling)"
BANELING_HUNTER_STRAIN = "Hunter Strain (Baneling)"
SWARM_HOST_CARRION_STRAIN = "Carrion Strain (Swarm Host)"
SWARM_HOST_CREEPER_STRAIN = "Creeper Strain (Swarm Host)"
ULTRALISK_NOXIOUS_STRAIN = "Noxious Strain (Ultralisk)"
ULTRALISK_TORRASQUE_STRAIN = "Torrasque Strain (Ultralisk)"
# Morphs
ZERGLING_BANELING_ASPECT = "Baneling Aspect (Zergling)"
HYDRALISK_IMPALER_ASPECT = "Impaler Aspect (Hydralisk)"
HYDRALISK_LURKER_ASPECT = "Lurker Aspect (Hydralisk)"
MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT = "Brood Lord Aspect (Mutalisk/Corruptor)"
MUTALISK_CORRUPTOR_VIPER_ASPECT = "Viper Aspect (Mutalisk/Corruptor)"
MUTALISK_CORRUPTOR_GUARDIAN_ASPECT = "Guardian Aspect (Mutalisk/Corruptor)"
MUTALISK_CORRUPTOR_DEVOURER_ASPECT = "Devourer Aspect (Mutalisk/Corruptor)"
ROACH_RAVAGER_ASPECT = "Ravager Aspect (Roach)"
# Zerg Mercs
INFESTED_MEDICS = "Infested Medics"
INFESTED_SIEGE_TANKS = "Infested Siege Tanks"
INFESTED_BANSHEES = "Infested Banshees"
# Kerrigan Upgrades
KERRIGAN_KINETIC_BLAST = "Kinetic Blast (Kerrigan Tier 1)"
KERRIGAN_HEROIC_FORTITUDE = "Heroic Fortitude (Kerrigan Tier 1)"
KERRIGAN_LEAPING_STRIKE = "Leaping Strike (Kerrigan Tier 1)"
KERRIGAN_CRUSHING_GRIP = "Crushing Grip (Kerrigan Tier 2)"
KERRIGAN_CHAIN_REACTION = "Chain Reaction (Kerrigan Tier 2)"
KERRIGAN_PSIONIC_SHIFT = "Psionic Shift (Kerrigan Tier 2)"
KERRIGAN_WILD_MUTATION = "Wild Mutation (Kerrigan Tier 4)"
KERRIGAN_SPAWN_BANELINGS = "Spawn Banelings (Kerrigan Tier 4)"
KERRIGAN_MEND = "Mend (Kerrigan Tier 4)"
KERRIGAN_INFEST_BROODLINGS = "Infest Broodlings (Kerrigan Tier 6)"
KERRIGAN_FURY = "Fury (Kerrigan Tier 6)"
KERRIGAN_ABILITY_EFFICIENCY = "Ability Efficiency (Kerrigan Tier 6)"
KERRIGAN_APOCALYPSE = "Apocalypse (Kerrigan Tier 7)"
KERRIGAN_SPAWN_LEVIATHAN = "Spawn Leviathan (Kerrigan Tier 7)"
KERRIGAN_DROP_PODS = "Drop-Pods (Kerrigan Tier 7)"
KERRIGAN_PRIMAL_FORM = "Primal Form (Kerrigan)"
# Misc Upgrades
KERRIGAN_ZERGLING_RECONSTITUTION = "Zergling Reconstitution (Kerrigan Tier 3)"
KERRIGAN_IMPROVED_OVERLORDS = "Improved Overlords (Kerrigan Tier 3)"
KERRIGAN_AUTOMATED_EXTRACTORS = "Automated Extractors (Kerrigan Tier 3)"
KERRIGAN_TWIN_DRONES = "Twin Drones (Kerrigan Tier 5)"
KERRIGAN_MALIGNANT_CREEP = "Malignant Creep (Kerrigan Tier 5)"
KERRIGAN_VESPENE_EFFICIENCY = "Vespene Efficiency (Kerrigan Tier 5)"
OVERLORD_VENTRAL_SACS = "Ventral Sacs (Overlord)"
# Kerrigan Levels
KERRIGAN_LEVELS_1 = "1 Kerrigan Level"
KERRIGAN_LEVELS_2 = "2 Kerrigan Levels"
KERRIGAN_LEVELS_3 = "3 Kerrigan Levels"
KERRIGAN_LEVELS_4 = "4 Kerrigan Levels"
KERRIGAN_LEVELS_5 = "5 Kerrigan Levels"
KERRIGAN_LEVELS_6 = "6 Kerrigan Levels"
KERRIGAN_LEVELS_7 = "7 Kerrigan Levels"
KERRIGAN_LEVELS_8 = "8 Kerrigan Levels"
KERRIGAN_LEVELS_9 = "9 Kerrigan Levels"
KERRIGAN_LEVELS_10 = "10 Kerrigan Levels"
KERRIGAN_LEVELS_14 = "14 Kerrigan Levels"
KERRIGAN_LEVELS_35 = "35 Kerrigan Levels"
KERRIGAN_LEVELS_70 = "70 Kerrigan Levels"
# Protoss Units
ZEALOT = "Zealot"
STALKER = "Stalker"
HIGH_TEMPLAR = "High Templar"
DARK_TEMPLAR = "Dark Templar"
IMMORTAL = "Immortal"
COLOSSUS = "Colossus"
PHOENIX = "Phoenix"
VOID_RAY = "Void Ray"
CARRIER = "Carrier"
OBSERVER = "Observer"
CENTURION = "Centurion"
SENTINEL = "Sentinel"
SUPPLICANT = "Supplicant"
INSTIGATOR = "Instigator"
SLAYER = "Slayer"
SENTRY = "Sentry"
ENERGIZER = "Energizer"
HAVOC = "Havoc"
SIGNIFIER = "Signifier"
ASCENDANT = "Ascendant"
AVENGER = "Avenger"
BLOOD_HUNTER = "Blood Hunter"
DRAGOON = "Dragoon"
DARK_ARCHON = "Dark Archon"
ADEPT = "Adept"
WARP_PRISM = "Warp Prism"
ANNIHILATOR = "Annihilator"
VANGUARD = "Vanguard"
WRATHWALKER = "Wrathwalker"
REAVER = "Reaver"
DISRUPTOR = "Disruptor"
MIRAGE = "Mirage"
CORSAIR = "Corsair"
DESTROYER = "Destroyer"
SCOUT = "Scout"
TEMPEST = "Tempest"
MOTHERSHIP = "Mothership"
ARBITER = "Arbiter"
ORACLE = "Oracle"
# Upgrades
PROTOSS_UPGRADE_PREFIX = "Progressive Protoss"
PROTOSS_GROUND_UPGRADE_PREFIX = f"{PROTOSS_UPGRADE_PREFIX} Ground"
PROTOSS_AIR_UPGRADE_PREFIX = f"{PROTOSS_UPGRADE_PREFIX} Air"
PROGRESSIVE_PROTOSS_GROUND_WEAPON = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_PROTOSS_GROUND_ARMOR = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Armor"
PROGRESSIVE_PROTOSS_SHIELDS = f"{PROTOSS_UPGRADE_PREFIX} Shields"
PROGRESSIVE_PROTOSS_AIR_WEAPON = f"{PROTOSS_AIR_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_PROTOSS_AIR_ARMOR = f"{PROTOSS_AIR_UPGRADE_PREFIX} Armor"
PROGRESSIVE_PROTOSS_WEAPON_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Weapon Upgrade"
PROGRESSIVE_PROTOSS_ARMOR_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Armor Upgrade"
PROGRESSIVE_PROTOSS_GROUND_UPGRADE = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_PROTOSS_AIR_UPGRADE = f"{PROTOSS_AIR_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Weapon/Armor Upgrade"
# Buildings
PHOTON_CANNON = "Photon Cannon"
KHAYDARIN_MONOLITH = "Khaydarin Monolith"
SHIELD_BATTERY = "Shield Battery"
# Unit Upgrades
SUPPLICANT_BLOOD_SHIELD = "Blood Shield (Supplicant)"
SUPPLICANT_SOUL_AUGMENTATION = "Soul Augmentation (Supplicant)"
SUPPLICANT_SHIELD_REGENERATION = "Shield Regeneration (Supplicant)"
ADEPT_SHOCKWAVE = "Shockwave (Adept)"
ADEPT_RESONATING_GLAIVES = "Resonating Glaives (Adept)"
ADEPT_PHASE_BULWARK = "Phase Bulwark (Adept)"
STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES = "Disintegrating Particles (Stalker/Instigator/Slayer)"
STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION = "Particle Reflection (Stalker/Instigator/Slayer)"
DRAGOON_HIGH_IMPACT_PHASE_DISRUPTORS = "High Impact Phase Disruptor (Dragoon)"
DRAGOON_TRILLIC_COMPRESSION_SYSTEM = "Trillic Compression System (Dragoon)"
DRAGOON_SINGULARITY_CHARGE = "Singularity Charge (Dragoon)"
DRAGOON_ENHANCED_STRIDER_SERVOS = "Enhanced Strider Servos (Dragoon)"
SCOUT_COMBAT_SENSOR_ARRAY = "Combat Sensor Array (Scout)"
SCOUT_APIAL_SENSORS = "Apial Sensors (Scout)"
SCOUT_GRAVITIC_THRUSTERS = "Gravitic Thrusters (Scout)"
SCOUT_ADVANCED_PHOTON_BLASTERS = "Advanced Photon Blasters (Scout)"
TEMPEST_TECTONIC_DESTABILIZERS = "Tectonic Destabilizers (Tempest)"
TEMPEST_QUANTIC_REACTOR = "Quantic Reactor (Tempest)"
TEMPEST_GRAVITY_SLING = "Gravity Sling (Tempest)"
PHOENIX_MIRAGE_IONIC_WAVELENGTH_FLUX = "Ionic Wavelength Flux (Phoenix/Mirage)"
PHOENIX_MIRAGE_ANION_PULSE_CRYSTALS = "Anion Pulse-Crystals (Phoenix/Mirage)"
CORSAIR_STEALTH_DRIVE = "Stealth Drive (Corsair)"
CORSAIR_ARGUS_JEWEL = "Argus Jewel (Corsair)"
CORSAIR_SUSTAINING_DISRUPTION = "Sustaining Disruption (Corsair)"
CORSAIR_NEUTRON_SHIELDS = "Neutron Shields (Corsair)"
ORACLE_STEALTH_DRIVE = "Stealth Drive (Oracle)"
ORACLE_STASIS_CALIBRATION = "Stasis Calibration (Oracle)"
ORACLE_TEMPORAL_ACCELERATION_BEAM = "Temporal Acceleration Beam (Oracle)"
ARBITER_CHRONOSTATIC_REINFORCEMENT = "Chronostatic Reinforcement (Arbiter)"
ARBITER_KHAYDARIN_CORE = "Khaydarin Core (Arbiter)"
ARBITER_SPACETIME_ANCHOR = "Spacetime Anchor (Arbiter)"
ARBITER_RESOURCE_EFFICIENCY = "Resource Efficiency (Arbiter)"
ARBITER_ENHANCED_CLOAK_FIELD = "Enhanced Cloak Field (Arbiter)"
CARRIER_GRAVITON_CATAPULT = "Graviton Catapult (Carrier)"
CARRIER_HULL_OF_PAST_GLORIES = "Hull of Past Glories (Carrier)"
VOID_RAY_DESTROYER_FLUX_VANES = "Flux Vanes (Void Ray/Destroyer)"
DESTROYER_REFORGED_BLOODSHARD_CORE = "Reforged Bloodshard Core (Destroyer)"
WARP_PRISM_GRAVITIC_DRIVE = "Gravitic Drive (Warp Prism)"
WARP_PRISM_PHASE_BLASTER = "Phase Blaster (Warp Prism)"
WARP_PRISM_WAR_CONFIGURATION = "War Configuration (Warp Prism)"
OBSERVER_GRAVITIC_BOOSTERS = "Gravitic Boosters (Observer)"
OBSERVER_SENSOR_ARRAY = "Sensor Array (Observer)"
REAVER_SCARAB_DAMAGE = "Scarab Damage (Reaver)"
REAVER_SOLARITE_PAYLOAD = "Solarite Payload (Reaver)"
REAVER_REAVER_CAPACITY = "Reaver Capacity (Reaver)"
REAVER_RESOURCE_EFFICIENCY = "Resource Efficiency (Reaver)"
VANGUARD_AGONY_LAUNCHERS = "Agony Launchers (Vanguard)"
VANGUARD_MATTER_DISPERSION = "Matter Dispersion (Vanguard)"
IMMORTAL_ANNIHILATOR_SINGULARITY_CHARGE = "Singularity Charge (Immortal/Annihilator)"
IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS = "Advanced Targeting Mechanics (Immortal/Annihilator)"
COLOSSUS_PACIFICATION_PROTOCOL = "Pacification Protocol (Colossus)"
WRATHWALKER_RAPID_POWER_CYCLING = "Rapid Power Cycling (Wrathwalker)"
WRATHWALKER_EYE_OF_WRATH = "Eye of Wrath (Wrathwalker)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHROUD_OF_ADUN = "Shroud of Adun (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHADOW_GUARD_TRAINING = "Shadow Guard Training (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK = "Blink (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_RESOURCE_EFFICIENCY = "Resource Efficiency (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_DARK_ARCHON_MELD = "Dark Archon Meld (Dark Templar)"
HIGH_TEMPLAR_SIGNIFIER_UNSHACKLED_PSIONIC_STORM = "Unshackled Psionic Storm (High Templar/Signifier)"
HIGH_TEMPLAR_SIGNIFIER_HALLUCINATION = "Hallucination (High Templar/Signifier)"
HIGH_TEMPLAR_SIGNIFIER_KHAYDARIN_AMULET = "Khaydarin Amulet (High Templar/Signifier)"
ARCHON_HIGH_ARCHON = "High Archon (Archon)"
DARK_ARCHON_FEEDBACK = "Feedback (Dark Archon)"
DARK_ARCHON_MAELSTROM = "Maelstrom (Dark Archon)"
DARK_ARCHON_ARGUS_TALISMAN = "Argus Talisman (Dark Archon)"
ASCENDANT_POWER_OVERWHELMING = "Power Overwhelming (Ascendant)"
ASCENDANT_CHAOTIC_ATTUNEMENT = "Chaotic Attunement (Ascendant)"
ASCENDANT_BLOOD_AMULET = "Blood Amulet (Ascendant)"
SENTRY_ENERGIZER_HAVOC_CLOAKING_MODULE = "Cloaking Module (Sentry/Energizer/Havoc)"
SENTRY_ENERGIZER_HAVOC_SHIELD_BATTERY_RAPID_RECHARGING = "Rapid Recharging (Sentry/Energizer/Havoc/Shield Battery)"
SENTRY_FORCE_FIELD = "Force Field (Sentry)"
SENTRY_HALLUCINATION = "Hallucination (Sentry)"
ENERGIZER_RECLAMATION = "Reclamation (Energizer)"
ENERGIZER_FORGED_CHASSIS = "Forged Chassis (Energizer)"
HAVOC_DETECT_WEAKNESS = "Detect Weakness (Havoc)"
HAVOC_BLOODSHARD_RESONANCE = "Bloodshard Resonance (Havoc)"
ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS = "Leg Enhancements (Zealot/Sentinel/Centurion)"
ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY = "Shield Capacity (Zealot/Sentinel/Centurion)"
# Spear Of Adun
SOA_CHRONO_SURGE = "Chrono Surge (Spear of Adun Calldown)"
SOA_PROGRESSIVE_PROXY_PYLON = "Progressive Proxy Pylon (Spear of Adun Calldown)"
SOA_PYLON_OVERCHARGE = "Pylon Overcharge (Spear of Adun Calldown)"
SOA_ORBITAL_STRIKE = "Orbital Strike (Spear of Adun Calldown)"
SOA_TEMPORAL_FIELD = "Temporal Field (Spear of Adun Calldown)"
SOA_SOLAR_LANCE = "Solar Lance (Spear of Adun Calldown)"
SOA_MASS_RECALL = "Mass Recall (Spear of Adun Calldown)"
SOA_SHIELD_OVERCHARGE = "Shield Overcharge (Spear of Adun Calldown)"
SOA_DEPLOY_FENIX = "Deploy Fenix (Spear of Adun Calldown)"
SOA_PURIFIER_BEAM = "Purifier Beam (Spear of Adun Calldown)"
SOA_TIME_STOP = "Time Stop (Spear of Adun Calldown)"
SOA_SOLAR_BOMBARDMENT = "Solar Bombardment (Spear of Adun Calldown)"
# Generic upgrades
MATRIX_OVERLOAD = "Matrix Overload"
QUATRO = "Quatro"
NEXUS_OVERCHARGE = "Nexus Overcharge"
ORBITAL_ASSIMILATORS = "Orbital Assimilators"
WARP_HARMONIZATION = "Warp Harmonization"
GUARDIAN_SHELL = "Guardian Shell"
RECONSTRUCTION_BEAM = "Reconstruction Beam (Spear of Adun Auto-Cast)"
OVERWATCH = "Overwatch (Spear of Adun Auto-Cast)"
SUPERIOR_WARP_GATES = "Superior Warp Gates"
ENHANCED_TARGETING = "Enhanced Targeting"
OPTIMIZED_ORDNANCE = "Optimized Ordnance"
KHALAI_INGENUITY = "Khalai Ingenuity"
AMPLIFIED_ASSIMILATORS = "Amplified Assimilators"
# Filler items
STARTING_MINERALS = "Additional Starting Minerals"
STARTING_VESPENE = "Additional Starting Vespene"
STARTING_SUPPLY = "Additional Starting Supply"
NOTHING = "Nothing"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,739 +0,0 @@
from typing import NamedTuple, Dict, List, Set, Union, Literal, Iterable, Callable
from enum import IntEnum, Enum
class SC2Race(IntEnum):
ANY = 0
TERRAN = 1
ZERG = 2
PROTOSS = 3
class MissionPools(IntEnum):
STARTER = 0
EASY = 1
MEDIUM = 2
HARD = 3
VERY_HARD = 4
FINAL = 5
class SC2CampaignGoalPriority(IntEnum):
"""
Campaign's priority to goal election
"""
NONE = 0
MINI_CAMPAIGN = 1 # A goal shouldn't be in a mini-campaign if there's at least one 'big' campaign
HARD = 2 # A campaign ending with a hard mission
VERY_HARD = 3 # A campaign ending with a very hard mission
EPILOGUE = 4 # Epilogue shall be always preferred as the goal if present
class SC2Campaign(Enum):
def __new__(cls, *args, **kwargs):
value = len(cls.__members__) + 1
obj = object.__new__(cls)
obj._value_ = value
return obj
def __init__(self, campaign_id: int, name: str, goal_priority: SC2CampaignGoalPriority, race: SC2Race):
self.id = campaign_id
self.campaign_name = name
self.goal_priority = goal_priority
self.race = race
def __lt__(self, other: "SC2Campaign"):
return self.id < other.id
GLOBAL = 0, "Global", SC2CampaignGoalPriority.NONE, SC2Race.ANY
WOL = 1, "Wings of Liberty", SC2CampaignGoalPriority.VERY_HARD, SC2Race.TERRAN
PROPHECY = 2, "Prophecy", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS
HOTS = 3, "Heart of the Swarm", SC2CampaignGoalPriority.HARD, SC2Race.ZERG
PROLOGUE = 4, "Whispers of Oblivion (Legacy of the Void: Prologue)", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS
LOTV = 5, "Legacy of the Void", SC2CampaignGoalPriority.VERY_HARD, SC2Race.PROTOSS
EPILOGUE = 6, "Into the Void (Legacy of the Void: Epilogue)", SC2CampaignGoalPriority.EPILOGUE, SC2Race.ANY
NCO = 7, "Nova Covert Ops", SC2CampaignGoalPriority.HARD, SC2Race.TERRAN
class SC2Mission(Enum):
def __new__(cls, *args, **kwargs):
value = len(cls.__members__) + 1
obj = object.__new__(cls)
obj._value_ = value
return obj
def __init__(self, mission_id: int, name: str, campaign: SC2Campaign, area: str, race: SC2Race, pool: MissionPools, map_file: str, build: bool = True):
self.id = mission_id
self.mission_name = name
self.campaign = campaign
self.area = area
self.race = race
self.pool = pool
self.map_file = map_file
self.build = build
# Wings of Liberty
LIBERATION_DAY = 1, "Liberation Day", SC2Campaign.WOL, "Mar Sara", SC2Race.ANY, MissionPools.STARTER, "ap_liberation_day", False
THE_OUTLAWS = 2, "The Outlaws", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_the_outlaws"
ZERO_HOUR = 3, "Zero Hour", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_zero_hour"
EVACUATION = 4, "Evacuation", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_evacuation"
OUTBREAK = 5, "Outbreak", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_outbreak"
SAFE_HAVEN = 6, "Safe Haven", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_safe_haven"
HAVENS_FALL = 7, "Haven's Fall", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_havens_fall"
SMASH_AND_GRAB = 8, "Smash and Grab", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.EASY, "ap_smash_and_grab"
THE_DIG = 9, "The Dig", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_dig"
THE_MOEBIUS_FACTOR = 10, "The Moebius Factor", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_moebius_factor"
SUPERNOVA = 11, "Supernova", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.HARD, "ap_supernova"
MAW_OF_THE_VOID = 12, "Maw of the Void", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.HARD, "ap_maw_of_the_void"
DEVILS_PLAYGROUND = 13, "Devil's Playground", SC2Campaign.WOL, "Covert", SC2Race.TERRAN, MissionPools.EASY, "ap_devils_playground"
WELCOME_TO_THE_JUNGLE = 14, "Welcome to the Jungle", SC2Campaign.WOL, "Covert", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_welcome_to_the_jungle"
BREAKOUT = 15, "Breakout", SC2Campaign.WOL, "Covert", SC2Race.ANY, MissionPools.STARTER, "ap_breakout", False
GHOST_OF_A_CHANCE = 16, "Ghost of a Chance", SC2Campaign.WOL, "Covert", SC2Race.ANY, MissionPools.STARTER, "ap_ghost_of_a_chance", False
THE_GREAT_TRAIN_ROBBERY = 17, "The Great Train Robbery", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_great_train_robbery"
CUTTHROAT = 18, "Cutthroat", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_cutthroat"
ENGINE_OF_DESTRUCTION = 19, "Engine of Destruction", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.HARD, "ap_engine_of_destruction"
MEDIA_BLITZ = 20, "Media Blitz", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_media_blitz"
PIERCING_OF_THE_SHROUD = 21, "Piercing the Shroud", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.STARTER, "ap_piercing_the_shroud", False
GATES_OF_HELL = 26, "Gates of Hell", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.HARD, "ap_gates_of_hell"
BELLY_OF_THE_BEAST = 27, "Belly of the Beast", SC2Campaign.WOL, "Char", SC2Race.ANY, MissionPools.STARTER, "ap_belly_of_the_beast", False
SHATTER_THE_SKY = 28, "Shatter the Sky", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.HARD, "ap_shatter_the_sky"
ALL_IN = 29, "All-In", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_all_in"
# Prophecy
WHISPERS_OF_DOOM = 22, "Whispers of Doom", SC2Campaign.PROPHECY, "_1", SC2Race.ANY, MissionPools.STARTER, "ap_whispers_of_doom", False
A_SINISTER_TURN = 23, "A Sinister Turn", SC2Campaign.PROPHECY, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_a_sinister_turn"
ECHOES_OF_THE_FUTURE = 24, "Echoes of the Future", SC2Campaign.PROPHECY, "_3", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_echoes_of_the_future"
IN_UTTER_DARKNESS = 25, "In Utter Darkness", SC2Campaign.PROPHECY, "_4", SC2Race.PROTOSS, MissionPools.HARD, "ap_in_utter_darkness"
# Heart of the Swarm
LAB_RAT = 30, "Lab Rat", SC2Campaign.HOTS, "Umoja", SC2Race.ZERG, MissionPools.STARTER, "ap_lab_rat"
BACK_IN_THE_SADDLE = 31, "Back in the Saddle", SC2Campaign.HOTS, "Umoja", SC2Race.ANY, MissionPools.STARTER, "ap_back_in_the_saddle", False
RENDEZVOUS = 32, "Rendezvous", SC2Campaign.HOTS, "Umoja", SC2Race.ZERG, MissionPools.EASY, "ap_rendezvous"
HARVEST_OF_SCREAMS = 33, "Harvest of Screams", SC2Campaign.HOTS, "Kaldir", SC2Race.ZERG, MissionPools.EASY, "ap_harvest_of_screams"
SHOOT_THE_MESSENGER = 34, "Shoot the Messenger", SC2Campaign.HOTS, "Kaldir", SC2Race.ZERG, MissionPools.EASY, "ap_shoot_the_messenger"
ENEMY_WITHIN = 35, "Enemy Within", SC2Campaign.HOTS, "Kaldir", SC2Race.ANY, MissionPools.EASY, "ap_enemy_within", False
DOMINATION = 36, "Domination", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.EASY, "ap_domination"
FIRE_IN_THE_SKY = 37, "Fire in the Sky", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.MEDIUM, "ap_fire_in_the_sky"
OLD_SOLDIERS = 38, "Old Soldiers", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.MEDIUM, "ap_old_soldiers"
WAKING_THE_ANCIENT = 39, "Waking the Ancient", SC2Campaign.HOTS, "Zerus", SC2Race.ZERG, MissionPools.MEDIUM, "ap_waking_the_ancient"
THE_CRUCIBLE = 40, "The Crucible", SC2Campaign.HOTS, "Zerus", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_crucible"
SUPREME = 41, "Supreme", SC2Campaign.HOTS, "Zerus", SC2Race.ANY, MissionPools.MEDIUM, "ap_supreme", False
INFESTED = 42, "Infested", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.MEDIUM, "ap_infested"
HAND_OF_DARKNESS = 43, "Hand of Darkness", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.HARD, "ap_hand_of_darkness"
PHANTOMS_OF_THE_VOID = 44, "Phantoms of the Void", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.HARD, "ap_phantoms_of_the_void"
WITH_FRIENDS_LIKE_THESE = 45, "With Friends Like These", SC2Campaign.HOTS, "Dominion Space", SC2Race.ANY, MissionPools.STARTER, "ap_with_friends_like_these", False
CONVICTION = 46, "Conviction", SC2Campaign.HOTS, "Dominion Space", SC2Race.ANY, MissionPools.MEDIUM, "ap_conviction", False
PLANETFALL = 47, "Planetfall", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_planetfall"
DEATH_FROM_ABOVE = 48, "Death From Above", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_death_from_above"
THE_RECKONING = 49, "The Reckoning", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_the_reckoning"
# Prologue
DARK_WHISPERS = 50, "Dark Whispers", SC2Campaign.PROLOGUE, "_1", SC2Race.PROTOSS, MissionPools.EASY, "ap_dark_whispers"
GHOSTS_IN_THE_FOG = 51, "Ghosts in the Fog", SC2Campaign.PROLOGUE, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_ghosts_in_the_fog"
EVIL_AWOKEN = 52, "Evil Awoken", SC2Campaign.PROLOGUE, "_3", SC2Race.PROTOSS, MissionPools.STARTER, "ap_evil_awoken", False
# LotV
FOR_AIUR = 53, "For Aiur!", SC2Campaign.LOTV, "Aiur", SC2Race.ANY, MissionPools.STARTER, "ap_for_aiur", False
THE_GROWING_SHADOW = 54, "The Growing Shadow", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_growing_shadow"
THE_SPEAR_OF_ADUN = 55, "The Spear of Adun", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_spear_of_adun"
SKY_SHIELD = 56, "Sky Shield", SC2Campaign.LOTV, "Korhal", SC2Race.PROTOSS, MissionPools.EASY, "ap_sky_shield"
BROTHERS_IN_ARMS = 57, "Brothers in Arms", SC2Campaign.LOTV, "Korhal", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_brothers_in_arms"
AMON_S_REACH = 58, "Amon's Reach", SC2Campaign.LOTV, "Shakuras", SC2Race.PROTOSS, MissionPools.EASY, "ap_amon_s_reach"
LAST_STAND = 59, "Last Stand", SC2Campaign.LOTV, "Shakuras", SC2Race.PROTOSS, MissionPools.HARD, "ap_last_stand"
FORBIDDEN_WEAPON = 60, "Forbidden Weapon", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_forbidden_weapon"
TEMPLE_OF_UNIFICATION = 61, "Temple of Unification", SC2Campaign.LOTV, "Ulnar", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_temple_of_unification"
THE_INFINITE_CYCLE = 62, "The Infinite Cycle", SC2Campaign.LOTV, "Ulnar", SC2Race.ANY, MissionPools.HARD, "ap_the_infinite_cycle", False
HARBINGER_OF_OBLIVION = 63, "Harbinger of Oblivion", SC2Campaign.LOTV, "Ulnar", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_harbinger_of_oblivion"
UNSEALING_THE_PAST = 64, "Unsealing the Past", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_unsealing_the_past"
PURIFICATION = 65, "Purification", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.HARD, "ap_purification"
STEPS_OF_THE_RITE = 66, "Steps of the Rite", SC2Campaign.LOTV, "Tal'darim", SC2Race.PROTOSS, MissionPools.HARD, "ap_steps_of_the_rite"
RAK_SHIR = 67, "Rak'Shir", SC2Campaign.LOTV, "Tal'darim", SC2Race.PROTOSS, MissionPools.HARD, "ap_rak_shir"
TEMPLAR_S_CHARGE = 68, "Templar's Charge", SC2Campaign.LOTV, "Moebius", SC2Race.PROTOSS, MissionPools.HARD, "ap_templar_s_charge"
TEMPLAR_S_RETURN = 69, "Templar's Return", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_templar_s_return", False
THE_HOST = 70, "The Host", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.HARD, "ap_the_host",
SALVATION = 71, "Salvation", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_salvation"
# Epilogue
INTO_THE_VOID = 72, "Into the Void", SC2Campaign.EPILOGUE, "_1", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_into_the_void"
THE_ESSENCE_OF_ETERNITY = 73, "The Essence of Eternity", SC2Campaign.EPILOGUE, "_2", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_the_essence_of_eternity"
AMON_S_FALL = 74, "Amon's Fall", SC2Campaign.EPILOGUE, "_3", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_amon_s_fall"
# Nova Covert Ops
THE_ESCAPE = 75, "The Escape", SC2Campaign.NCO, "_1", SC2Race.ANY, MissionPools.MEDIUM, "ap_the_escape", False
SUDDEN_STRIKE = 76, "Sudden Strike", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.EASY, "ap_sudden_strike"
ENEMY_INTELLIGENCE = 77, "Enemy Intelligence", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_enemy_intelligence"
TROUBLE_IN_PARADISE = 78, "Trouble In Paradise", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_trouble_in_paradise"
NIGHT_TERRORS = 79, "Night Terrors", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_night_terrors"
FLASHPOINT = 80, "Flashpoint", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_flashpoint"
IN_THE_ENEMY_S_SHADOW = 81, "In the Enemy's Shadow", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_in_the_enemy_s_shadow", False
DARK_SKIES = 82, "Dark Skies", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.HARD, "ap_dark_skies"
END_GAME = 83, "End Game", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_end_game"
class MissionConnection:
campaign: SC2Campaign
connect_to: int # -1 connects to Menu
def __init__(self, connect_to, campaign = SC2Campaign.GLOBAL):
self.campaign = campaign
self.connect_to = connect_to
def _asdict(self):
return {
"campaign": self.campaign.id,
"connect_to": self.connect_to
}
class MissionInfo(NamedTuple):
mission: SC2Mission
required_world: List[Union[MissionConnection, Dict[Literal["campaign", "connect_to"], int]]]
category: str
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
ui_vertical_padding: int = 0
class FillMission(NamedTuple):
type: MissionPools
connect_to: List[MissionConnection]
category: str
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
removal_priority: int = 0 # how many missions missing from the pool required to remove this mission
def vanilla_shuffle_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.WOL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.WOL)], "Colonist"),
FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.WOL)], "Colonist"),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.WOL)], "Colonist", number=7),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.WOL)], "Colonist", number=7, removal_priority=1),
FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.WOL)], "Artifact", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(7, SC2Campaign.WOL)], "Artifact", number=8, completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(8, SC2Campaign.WOL)], "Artifact", number=11, completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(9, SC2Campaign.WOL)], "Artifact", number=14, completion_critical=True, removal_priority=7),
FillMission(MissionPools.HARD, [MissionConnection(10, SC2Campaign.WOL)], "Artifact", completion_critical=True, removal_priority=6),
FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.WOL)], "Covert", number=4),
FillMission(MissionPools.MEDIUM, [MissionConnection(12, SC2Campaign.WOL)], "Covert"),
FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.WOL)], "Covert", number=8, removal_priority=3),
FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.WOL)], "Covert", number=8, removal_priority=2),
FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.WOL)], "Rebellion", number=6),
FillMission(MissionPools.HARD, [MissionConnection(16, SC2Campaign.WOL)], "Rebellion"),
FillMission(MissionPools.HARD, [MissionConnection(17, SC2Campaign.WOL)], "Rebellion"),
FillMission(MissionPools.HARD, [MissionConnection(18, SC2Campaign.WOL)], "Rebellion", removal_priority=8),
FillMission(MissionPools.HARD, [MissionConnection(19, SC2Campaign.WOL)], "Rebellion", removal_priority=5),
FillMission(MissionPools.HARD, [MissionConnection(11, SC2Campaign.WOL)], "Char", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(21, SC2Campaign.WOL)], "Char", completion_critical=True, removal_priority=4),
FillMission(MissionPools.HARD, [MissionConnection(21, SC2Campaign.WOL)], "Char", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(22, SC2Campaign.WOL), MissionConnection(23, SC2Campaign.WOL)], "Char", completion_critical=True, or_requirements=True)
],
SC2Campaign.PROPHECY: [
FillMission(MissionPools.MEDIUM, [MissionConnection(8, SC2Campaign.WOL)], "_1"),
FillMission(MissionPools.HARD, [MissionConnection(0, SC2Campaign.PROPHECY)], "_2", removal_priority=2),
FillMission(MissionPools.HARD, [MissionConnection(1, SC2Campaign.PROPHECY)], "_3", removal_priority=1),
FillMission(MissionPools.FINAL, [MissionConnection(2, SC2Campaign.PROPHECY)], "_4"),
],
SC2Campaign.HOTS: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.HOTS)], "Umoja", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.HOTS)], "Umoja", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.HOTS)], "Umoja", completion_critical=True, removal_priority=1),
FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.HOTS)], "Kaldir", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.HOTS)], "Kaldir", completion_critical=True, removal_priority=2),
FillMission(MissionPools.MEDIUM, [MissionConnection(4, SC2Campaign.HOTS)], "Kaldir", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.HOTS)], "Char", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(6, SC2Campaign.HOTS)], "Char", completion_critical=True, removal_priority=3),
FillMission(MissionPools.MEDIUM, [MissionConnection(7, SC2Campaign.HOTS)], "Char", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS), MissionConnection(8, SC2Campaign.HOTS)], "Zerus", completion_critical=True, or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(9, SC2Campaign.HOTS)], "Zerus", completion_critical=True, removal_priority=4),
FillMission(MissionPools.MEDIUM, [MissionConnection(10, SC2Campaign.HOTS)], "Zerus", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS), MissionConnection(8, SC2Campaign.HOTS), MissionConnection(11, SC2Campaign.HOTS)], "Skygeirr Station", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(12, SC2Campaign.HOTS)], "Skygeirr Station", completion_critical=True, removal_priority=5),
FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.HOTS)], "Skygeirr Station", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS), MissionConnection(8, SC2Campaign.HOTS), MissionConnection(11, SC2Campaign.HOTS)], "Dominion Space", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(15, SC2Campaign.HOTS)], "Dominion Space", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(14, SC2Campaign.HOTS), MissionConnection(16, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(17, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(18, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
],
SC2Campaign.PROLOGUE: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.PROLOGUE)], "_1"),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.PROLOGUE)], "_2", removal_priority=1),
FillMission(MissionPools.FINAL, [MissionConnection(1, SC2Campaign.PROLOGUE)], "_3")
],
SC2Campaign.LOTV: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.LOTV)], "Aiur", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.LOTV)], "Aiur", completion_critical=True, removal_priority=3),
FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.LOTV)], "Aiur", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.LOTV)], "Korhal", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.LOTV)], "Korhal", completion_critical=True, removal_priority=7),
FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.LOTV)], "Shakuras", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.LOTV)], "Shakuras", completion_critical=True, removal_priority=6),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.LOTV), MissionConnection(6, SC2Campaign.LOTV)], "Purifier", completion_critical=True, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.LOTV), MissionConnection(6, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV)], "Ulnar", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(8, SC2Campaign.LOTV)], "Ulnar", completion_critical=True, removal_priority=1),
FillMission(MissionPools.HARD, [MissionConnection(9, SC2Campaign.LOTV)], "Ulnar", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(10, SC2Campaign.LOTV)], "Purifier", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(11, SC2Campaign.LOTV)], "Purifier", completion_critical=True, removal_priority=5),
FillMission(MissionPools.HARD, [MissionConnection(10, SC2Campaign.LOTV)], "Tal'darim", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.LOTV)], "Tal'darim", completion_critical=True, removal_priority=4),
FillMission(MissionPools.HARD, [MissionConnection(12, SC2Campaign.LOTV), MissionConnection(14, SC2Campaign.LOTV)], "Moebius", completion_critical=True, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(12, SC2Campaign.LOTV), MissionConnection(14, SC2Campaign.LOTV), MissionConnection(15, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(16, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True, removal_priority=2),
FillMission(MissionPools.FINAL, [MissionConnection(17, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
],
SC2Campaign.EPILOGUE: [
FillMission(MissionPools.VERY_HARD, [MissionConnection(24, SC2Campaign.WOL), MissionConnection(19, SC2Campaign.HOTS), MissionConnection(18, SC2Campaign.LOTV)], "_1", completion_critical=True),
FillMission(MissionPools.VERY_HARD, [MissionConnection(0, SC2Campaign.EPILOGUE)], "_2", completion_critical=True, removal_priority=1),
FillMission(MissionPools.FINAL, [MissionConnection(1, SC2Campaign.EPILOGUE)], "_3", completion_critical=True),
],
SC2Campaign.NCO: [
FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.NCO)], "_1", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.NCO)], "_1", completion_critical=True, removal_priority=6),
FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.NCO)], "_1", completion_critical=True, removal_priority=5),
FillMission(MissionPools.HARD, [MissionConnection(2, SC2Campaign.NCO)], "_2", completion_critical=True, removal_priority=7),
FillMission(MissionPools.HARD, [MissionConnection(3, SC2Campaign.NCO)], "_2", completion_critical=True, removal_priority=4),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.NCO)], "_2", completion_critical=True, removal_priority=3),
FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.NCO)], "_3", completion_critical=True, removal_priority=2),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.NCO)], "_3", completion_critical=True, removal_priority=1),
FillMission(MissionPools.FINAL, [MissionConnection(7, SC2Campaign.NCO)], "_3", completion_critical=True),
]
}
def mini_campaign_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.WOL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.WOL)], "Colonist"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.WOL)], "Colonist"),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.WOL)], "Artifact", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.WOL)], "Artifact", number=4, completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.WOL)], "Artifact", number=8, completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.WOL)], "Covert", number=2),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.WOL)], "Covert"),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.WOL)], "Rebellion", number=3),
FillMission(MissionPools.HARD, [MissionConnection(8, SC2Campaign.WOL)], "Rebellion"),
FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.WOL)], "Char", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.WOL)], "Char", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(10, SC2Campaign.WOL), MissionConnection(11, SC2Campaign.WOL)], "Char", completion_critical=True, or_requirements=True)
],
SC2Campaign.PROPHECY: [
FillMission(MissionPools.MEDIUM, [MissionConnection(4, SC2Campaign.WOL)], "_1"),
FillMission(MissionPools.FINAL, [MissionConnection(0, SC2Campaign.PROPHECY)], "_2"),
],
SC2Campaign.HOTS: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.HOTS)], "Umoja", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.HOTS)], "Kaldir"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.HOTS)], "Kaldir"),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.HOTS)], "Char"),
FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.HOTS)], "Char"),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.HOTS)], "Zerus", number=3),
FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS)], "Zerus"),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.HOTS)], "Skygeirr Station", number=5),
FillMission(MissionPools.HARD, [MissionConnection(7, SC2Campaign.HOTS)], "Skygeirr Station"),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.HOTS)], "Dominion Space", number=5),
FillMission(MissionPools.HARD, [MissionConnection(9, SC2Campaign.HOTS)], "Dominion Space"),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.HOTS)], "Korhal", completion_critical=True, number=8),
FillMission(MissionPools.FINAL, [MissionConnection(11, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
],
SC2Campaign.PROLOGUE: [
FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.PROLOGUE)], "_1"),
FillMission(MissionPools.FINAL, [MissionConnection(0, SC2Campaign.PROLOGUE)], "_2")
],
SC2Campaign.LOTV: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.LOTV)], "Aiur",completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.LOTV)], "Aiur", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.LOTV)], "Korhal", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.LOTV)], "Shakuras", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.LOTV), MissionConnection(3, SC2Campaign.LOTV)], "Purifier", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.LOTV)], "Purifier", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.LOTV)], "Ulnar", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.LOTV)], "Tal'darim", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(8, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
],
SC2Campaign.EPILOGUE: [
FillMission(MissionPools.VERY_HARD, [MissionConnection(12, SC2Campaign.WOL), MissionConnection(12, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.LOTV)], "_1", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(0, SC2Campaign.EPILOGUE)], "_2", completion_critical=True),
],
SC2Campaign.NCO: [
FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.NCO)], "_1", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.NCO)], "_1", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.NCO)], "_2", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(2, SC2Campaign.NCO)], "_3", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(3, SC2Campaign.NCO)], "_3", completion_critical=True),
]
}
def gauntlet_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "I", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0)], "II", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(1)], "III", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(2)], "IV", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(3)], "V", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(4)], "VI", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(5)], "Final", completion_critical=True)
]
}
def mini_gauntlet_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "I", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0)], "II", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(1)], "III", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(2)], "Final", completion_critical=True)
]
}
def grid_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "_1"),
FillMission(MissionPools.EASY, [MissionConnection(0)], "_1"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(6), MissionConnection( 3)], "_1", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(2), MissionConnection(7)], "_1", or_requirements=True),
FillMission(MissionPools.EASY, [MissionConnection(0)], "_2"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(4)], "_2", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(2), MissionConnection(5), MissionConnection(10), MissionConnection(7)], "_2", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(3), MissionConnection(6), MissionConnection(11)], "_2", or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(4), MissionConnection(9), MissionConnection(12)], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(5), MissionConnection(8), MissionConnection(10), MissionConnection(13)], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(6), MissionConnection(9), MissionConnection(11), MissionConnection(14)], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(7), MissionConnection(10)], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(8), MissionConnection(13)], "_4", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(9), MissionConnection(12), MissionConnection(14)], "_4", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(10), MissionConnection(13)], "_4", or_requirements=True),
FillMission(MissionPools.FINAL, [MissionConnection(11), MissionConnection(14)], "_4", or_requirements=True)
]
}
def mini_grid_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "_1"),
FillMission(MissionPools.EASY, [MissionConnection(0)], "_1"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(5)], "_1", or_requirements=True),
FillMission(MissionPools.EASY, [MissionConnection(0)], "_2"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(3)], "_2", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(2), MissionConnection(4)], "_2", or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(3), MissionConnection(7)], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(4), MissionConnection(6)], "_3", or_requirements=True),
FillMission(MissionPools.FINAL, [MissionConnection(5), MissionConnection(7)], "_3", or_requirements=True)
]
}
def tiny_grid_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "_1"),
FillMission(MissionPools.MEDIUM, [MissionConnection(0)], "_1"),
FillMission(MissionPools.EASY, [MissionConnection(0)], "_2"),
FillMission(MissionPools.FINAL, [MissionConnection(1), MissionConnection(2)], "_2", or_requirements=True),
]
}
def blitz_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "I"),
FillMission(MissionPools.EASY, [MissionConnection(-1)], "I"),
FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "II", number=1, or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "II", number=1, or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "III", number=2, or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "III", number=2, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "IV", number=3, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "IV", number=3, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "V", number=4, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "V", number=4, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "Final", number=5, or_requirements=True),
FillMission(MissionPools.FINAL, [MissionConnection(0), MissionConnection(1)], "Final", number=5, or_requirements=True)
]
}
mission_orders: List[Callable[[], Dict[SC2Campaign, List[FillMission]]]] = [
vanilla_shuffle_order,
vanilla_shuffle_order,
mini_campaign_order,
grid_order,
mini_grid_order,
blitz_order,
gauntlet_order,
mini_gauntlet_order,
tiny_grid_order
]
vanilla_mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = {
SC2Campaign.WOL: {
SC2Mission.LIBERATION_DAY.mission_name: MissionInfo(SC2Mission.LIBERATION_DAY, [], SC2Mission.LIBERATION_DAY.area, completion_critical=True),
SC2Mission.THE_OUTLAWS.mission_name: MissionInfo(SC2Mission.THE_OUTLAWS, [MissionConnection(1, SC2Campaign.WOL)], SC2Mission.THE_OUTLAWS.area, completion_critical=True),
SC2Mission.ZERO_HOUR.mission_name: MissionInfo(SC2Mission.ZERO_HOUR, [MissionConnection(2, SC2Campaign.WOL)], SC2Mission.ZERO_HOUR.area, completion_critical=True),
SC2Mission.EVACUATION.mission_name: MissionInfo(SC2Mission.EVACUATION, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.EVACUATION.area),
SC2Mission.OUTBREAK.mission_name: MissionInfo(SC2Mission.OUTBREAK, [MissionConnection(4, SC2Campaign.WOL)], SC2Mission.OUTBREAK.area),
SC2Mission.SAFE_HAVEN.mission_name: MissionInfo(SC2Mission.SAFE_HAVEN, [MissionConnection(5, SC2Campaign.WOL)], SC2Mission.SAFE_HAVEN.area, number=7),
SC2Mission.HAVENS_FALL.mission_name: MissionInfo(SC2Mission.HAVENS_FALL, [MissionConnection(5, SC2Campaign.WOL)], SC2Mission.HAVENS_FALL.area, number=7),
SC2Mission.SMASH_AND_GRAB.mission_name: MissionInfo(SC2Mission.SMASH_AND_GRAB, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.SMASH_AND_GRAB.area, completion_critical=True),
SC2Mission.THE_DIG.mission_name: MissionInfo(SC2Mission.THE_DIG, [MissionConnection(8, SC2Campaign.WOL)], SC2Mission.THE_DIG.area, number=8, completion_critical=True),
SC2Mission.THE_MOEBIUS_FACTOR.mission_name: MissionInfo(SC2Mission.THE_MOEBIUS_FACTOR, [MissionConnection(9, SC2Campaign.WOL)], SC2Mission.THE_MOEBIUS_FACTOR.area, number=11, completion_critical=True),
SC2Mission.SUPERNOVA.mission_name: MissionInfo(SC2Mission.SUPERNOVA, [MissionConnection(10, SC2Campaign.WOL)], SC2Mission.SUPERNOVA.area, number=14, completion_critical=True),
SC2Mission.MAW_OF_THE_VOID.mission_name: MissionInfo(SC2Mission.MAW_OF_THE_VOID, [MissionConnection(11, SC2Campaign.WOL)], SC2Mission.MAW_OF_THE_VOID.area, completion_critical=True),
SC2Mission.DEVILS_PLAYGROUND.mission_name: MissionInfo(SC2Mission.DEVILS_PLAYGROUND, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.DEVILS_PLAYGROUND.area, number=4),
SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name: MissionInfo(SC2Mission.WELCOME_TO_THE_JUNGLE, [MissionConnection(13, SC2Campaign.WOL)], SC2Mission.WELCOME_TO_THE_JUNGLE.area),
SC2Mission.BREAKOUT.mission_name: MissionInfo(SC2Mission.BREAKOUT, [MissionConnection(14, SC2Campaign.WOL)], SC2Mission.BREAKOUT.area, number=8),
SC2Mission.GHOST_OF_A_CHANCE.mission_name: MissionInfo(SC2Mission.GHOST_OF_A_CHANCE, [MissionConnection(14, SC2Campaign.WOL)], SC2Mission.GHOST_OF_A_CHANCE.area, number=8),
SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name: MissionInfo(SC2Mission.THE_GREAT_TRAIN_ROBBERY, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.THE_GREAT_TRAIN_ROBBERY.area, number=6),
SC2Mission.CUTTHROAT.mission_name: MissionInfo(SC2Mission.CUTTHROAT, [MissionConnection(17, SC2Campaign.WOL)], SC2Mission.THE_GREAT_TRAIN_ROBBERY.area),
SC2Mission.ENGINE_OF_DESTRUCTION.mission_name: MissionInfo(SC2Mission.ENGINE_OF_DESTRUCTION, [MissionConnection(18, SC2Campaign.WOL)], SC2Mission.ENGINE_OF_DESTRUCTION.area),
SC2Mission.MEDIA_BLITZ.mission_name: MissionInfo(SC2Mission.MEDIA_BLITZ, [MissionConnection(19, SC2Campaign.WOL)], SC2Mission.MEDIA_BLITZ.area),
SC2Mission.PIERCING_OF_THE_SHROUD.mission_name: MissionInfo(SC2Mission.PIERCING_OF_THE_SHROUD, [MissionConnection(20, SC2Campaign.WOL)], SC2Mission.PIERCING_OF_THE_SHROUD.area),
SC2Mission.GATES_OF_HELL.mission_name: MissionInfo(SC2Mission.GATES_OF_HELL, [MissionConnection(12, SC2Campaign.WOL)], SC2Mission.GATES_OF_HELL.area, completion_critical=True),
SC2Mission.BELLY_OF_THE_BEAST.mission_name: MissionInfo(SC2Mission.BELLY_OF_THE_BEAST, [MissionConnection(22, SC2Campaign.WOL)], SC2Mission.BELLY_OF_THE_BEAST.area, completion_critical=True),
SC2Mission.SHATTER_THE_SKY.mission_name: MissionInfo(SC2Mission.SHATTER_THE_SKY, [MissionConnection(22, SC2Campaign.WOL)], SC2Mission.SHATTER_THE_SKY.area, completion_critical=True),
SC2Mission.ALL_IN.mission_name: MissionInfo(SC2Mission.ALL_IN, [MissionConnection(23, SC2Campaign.WOL), MissionConnection(24, SC2Campaign.WOL)], SC2Mission.ALL_IN.area, or_requirements=True, completion_critical=True)
},
SC2Campaign.PROPHECY: {
SC2Mission.WHISPERS_OF_DOOM.mission_name: MissionInfo(SC2Mission.WHISPERS_OF_DOOM, [MissionConnection(9, SC2Campaign.WOL)], SC2Mission.WHISPERS_OF_DOOM.area),
SC2Mission.A_SINISTER_TURN.mission_name: MissionInfo(SC2Mission.A_SINISTER_TURN, [MissionConnection(1, SC2Campaign.PROPHECY)], SC2Mission.A_SINISTER_TURN.area),
SC2Mission.ECHOES_OF_THE_FUTURE.mission_name: MissionInfo(SC2Mission.ECHOES_OF_THE_FUTURE, [MissionConnection(2, SC2Campaign.PROPHECY)], SC2Mission.ECHOES_OF_THE_FUTURE.area),
SC2Mission.IN_UTTER_DARKNESS.mission_name: MissionInfo(SC2Mission.IN_UTTER_DARKNESS, [MissionConnection(3, SC2Campaign.PROPHECY)], SC2Mission.IN_UTTER_DARKNESS.area)
},
SC2Campaign.HOTS: {
SC2Mission.LAB_RAT.mission_name: MissionInfo(SC2Mission.LAB_RAT, [], SC2Mission.LAB_RAT.area, completion_critical=True),
SC2Mission.BACK_IN_THE_SADDLE.mission_name: MissionInfo(SC2Mission.BACK_IN_THE_SADDLE, [MissionConnection(1, SC2Campaign.HOTS)], SC2Mission.BACK_IN_THE_SADDLE.area, completion_critical=True),
SC2Mission.RENDEZVOUS.mission_name: MissionInfo(SC2Mission.RENDEZVOUS, [MissionConnection(2, SC2Campaign.HOTS)], SC2Mission.RENDEZVOUS.area, completion_critical=True),
SC2Mission.HARVEST_OF_SCREAMS.mission_name: MissionInfo(SC2Mission.HARVEST_OF_SCREAMS, [MissionConnection(3, SC2Campaign.HOTS)], SC2Mission.HARVEST_OF_SCREAMS.area),
SC2Mission.SHOOT_THE_MESSENGER.mission_name: MissionInfo(SC2Mission.SHOOT_THE_MESSENGER, [MissionConnection(4, SC2Campaign.HOTS)], SC2Mission.SHOOT_THE_MESSENGER.area),
SC2Mission.ENEMY_WITHIN.mission_name: MissionInfo(SC2Mission.ENEMY_WITHIN, [MissionConnection(5, SC2Campaign.HOTS)], SC2Mission.ENEMY_WITHIN.area),
SC2Mission.DOMINATION.mission_name: MissionInfo(SC2Mission.DOMINATION, [MissionConnection(3, SC2Campaign.HOTS)], SC2Mission.DOMINATION.area),
SC2Mission.FIRE_IN_THE_SKY.mission_name: MissionInfo(SC2Mission.FIRE_IN_THE_SKY, [MissionConnection(7, SC2Campaign.HOTS)], SC2Mission.FIRE_IN_THE_SKY.area),
SC2Mission.OLD_SOLDIERS.mission_name: MissionInfo(SC2Mission.OLD_SOLDIERS, [MissionConnection(8, SC2Campaign.HOTS)], SC2Mission.OLD_SOLDIERS.area),
SC2Mission.WAKING_THE_ANCIENT.mission_name: MissionInfo(SC2Mission.WAKING_THE_ANCIENT, [MissionConnection(6, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.HOTS)], SC2Mission.WAKING_THE_ANCIENT.area, completion_critical=True, or_requirements=True),
SC2Mission.THE_CRUCIBLE.mission_name: MissionInfo(SC2Mission.THE_CRUCIBLE, [MissionConnection(10, SC2Campaign.HOTS)], SC2Mission.THE_CRUCIBLE.area, completion_critical=True),
SC2Mission.SUPREME.mission_name: MissionInfo(SC2Mission.SUPREME, [MissionConnection(11, SC2Campaign.HOTS)], SC2Mission.SUPREME.area, completion_critical=True),
SC2Mission.INFESTED.mission_name: MissionInfo(SC2Mission.INFESTED, [MissionConnection(6, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.HOTS), MissionConnection(12, SC2Campaign.HOTS)], SC2Mission.INFESTED.area),
SC2Mission.HAND_OF_DARKNESS.mission_name: MissionInfo(SC2Mission.HAND_OF_DARKNESS, [MissionConnection(13, SC2Campaign.HOTS)], SC2Mission.HAND_OF_DARKNESS.area),
SC2Mission.PHANTOMS_OF_THE_VOID.mission_name: MissionInfo(SC2Mission.PHANTOMS_OF_THE_VOID, [MissionConnection(14, SC2Campaign.HOTS)], SC2Mission.PHANTOMS_OF_THE_VOID.area),
SC2Mission.WITH_FRIENDS_LIKE_THESE.mission_name: MissionInfo(SC2Mission.WITH_FRIENDS_LIKE_THESE, [MissionConnection(6, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.HOTS), MissionConnection(12, SC2Campaign.HOTS)], SC2Mission.WITH_FRIENDS_LIKE_THESE.area),
SC2Mission.CONVICTION.mission_name: MissionInfo(SC2Mission.CONVICTION, [MissionConnection(16, SC2Campaign.HOTS)], SC2Mission.CONVICTION.area),
SC2Mission.PLANETFALL.mission_name: MissionInfo(SC2Mission.PLANETFALL, [MissionConnection(15, SC2Campaign.HOTS), MissionConnection(17, SC2Campaign.HOTS)], SC2Mission.PLANETFALL.area, completion_critical=True),
SC2Mission.DEATH_FROM_ABOVE.mission_name: MissionInfo(SC2Mission.DEATH_FROM_ABOVE, [MissionConnection(18, SC2Campaign.HOTS)], SC2Mission.DEATH_FROM_ABOVE.area, completion_critical=True),
SC2Mission.THE_RECKONING.mission_name: MissionInfo(SC2Mission.THE_RECKONING, [MissionConnection(19, SC2Campaign.HOTS)], SC2Mission.THE_RECKONING.area, completion_critical=True),
},
SC2Campaign.PROLOGUE: {
SC2Mission.DARK_WHISPERS.mission_name: MissionInfo(SC2Mission.DARK_WHISPERS, [], SC2Mission.DARK_WHISPERS.area),
SC2Mission.GHOSTS_IN_THE_FOG.mission_name: MissionInfo(SC2Mission.GHOSTS_IN_THE_FOG, [MissionConnection(1, SC2Campaign.PROLOGUE)], SC2Mission.GHOSTS_IN_THE_FOG.area),
SC2Mission.EVIL_AWOKEN.mission_name: MissionInfo(SC2Mission.EVIL_AWOKEN, [MissionConnection(2, SC2Campaign.PROLOGUE)], SC2Mission.EVIL_AWOKEN.area)
},
SC2Campaign.LOTV: {
SC2Mission.FOR_AIUR.mission_name: MissionInfo(SC2Mission.FOR_AIUR, [], SC2Mission.FOR_AIUR.area, completion_critical=True),
SC2Mission.THE_GROWING_SHADOW.mission_name: MissionInfo(SC2Mission.THE_GROWING_SHADOW, [MissionConnection(1, SC2Campaign.LOTV)], SC2Mission.THE_GROWING_SHADOW.area, completion_critical=True),
SC2Mission.THE_SPEAR_OF_ADUN.mission_name: MissionInfo(SC2Mission.THE_SPEAR_OF_ADUN, [MissionConnection(2, SC2Campaign.LOTV)], SC2Mission.THE_SPEAR_OF_ADUN.area, completion_critical=True),
SC2Mission.SKY_SHIELD.mission_name: MissionInfo(SC2Mission.SKY_SHIELD, [MissionConnection(3, SC2Campaign.LOTV)], SC2Mission.SKY_SHIELD.area, completion_critical=True),
SC2Mission.BROTHERS_IN_ARMS.mission_name: MissionInfo(SC2Mission.BROTHERS_IN_ARMS, [MissionConnection(4, SC2Campaign.LOTV)], SC2Mission.BROTHERS_IN_ARMS.area, completion_critical=True),
SC2Mission.AMON_S_REACH.mission_name: MissionInfo(SC2Mission.AMON_S_REACH, [MissionConnection(3, SC2Campaign.LOTV)], SC2Mission.AMON_S_REACH.area, completion_critical=True),
SC2Mission.LAST_STAND.mission_name: MissionInfo(SC2Mission.LAST_STAND, [MissionConnection(6, SC2Campaign.LOTV)], SC2Mission.LAST_STAND.area, completion_critical=True),
SC2Mission.FORBIDDEN_WEAPON.mission_name: MissionInfo(SC2Mission.FORBIDDEN_WEAPON, [MissionConnection(5, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV)], SC2Mission.FORBIDDEN_WEAPON.area, completion_critical=True, or_requirements=True),
SC2Mission.TEMPLE_OF_UNIFICATION.mission_name: MissionInfo(SC2Mission.TEMPLE_OF_UNIFICATION, [MissionConnection(5, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV), MissionConnection(8, SC2Campaign.LOTV)], SC2Mission.TEMPLE_OF_UNIFICATION.area, completion_critical=True),
SC2Mission.THE_INFINITE_CYCLE.mission_name: MissionInfo(SC2Mission.THE_INFINITE_CYCLE, [MissionConnection(9, SC2Campaign.LOTV)], SC2Mission.THE_INFINITE_CYCLE.area, completion_critical=True),
SC2Mission.HARBINGER_OF_OBLIVION.mission_name: MissionInfo(SC2Mission.HARBINGER_OF_OBLIVION, [MissionConnection(10, SC2Campaign.LOTV)], SC2Mission.HARBINGER_OF_OBLIVION.area, completion_critical=True),
SC2Mission.UNSEALING_THE_PAST.mission_name: MissionInfo(SC2Mission.UNSEALING_THE_PAST, [MissionConnection(11, SC2Campaign.LOTV)], SC2Mission.UNSEALING_THE_PAST.area, completion_critical=True),
SC2Mission.PURIFICATION.mission_name: MissionInfo(SC2Mission.PURIFICATION, [MissionConnection(12, SC2Campaign.LOTV)], SC2Mission.PURIFICATION.area, completion_critical=True),
SC2Mission.STEPS_OF_THE_RITE.mission_name: MissionInfo(SC2Mission.STEPS_OF_THE_RITE, [MissionConnection(11, SC2Campaign.LOTV)], SC2Mission.STEPS_OF_THE_RITE.area, completion_critical=True),
SC2Mission.RAK_SHIR.mission_name: MissionInfo(SC2Mission.RAK_SHIR, [MissionConnection(14, SC2Campaign.LOTV)], SC2Mission.RAK_SHIR.area, completion_critical=True),
SC2Mission.TEMPLAR_S_CHARGE.mission_name: MissionInfo(SC2Mission.TEMPLAR_S_CHARGE, [MissionConnection(13, SC2Campaign.LOTV), MissionConnection(15, SC2Campaign.LOTV)], SC2Mission.TEMPLAR_S_CHARGE.area, completion_critical=True, or_requirements=True),
SC2Mission.TEMPLAR_S_RETURN.mission_name: MissionInfo(SC2Mission.TEMPLAR_S_RETURN, [MissionConnection(13, SC2Campaign.LOTV), MissionConnection(15, SC2Campaign.LOTV), MissionConnection(16, SC2Campaign.LOTV)], SC2Mission.TEMPLAR_S_RETURN.area, completion_critical=True),
SC2Mission.THE_HOST.mission_name: MissionInfo(SC2Mission.THE_HOST, [MissionConnection(17, SC2Campaign.LOTV)], SC2Mission.THE_HOST.area, completion_critical=True),
SC2Mission.SALVATION.mission_name: MissionInfo(SC2Mission.SALVATION, [MissionConnection(18, SC2Campaign.LOTV)], SC2Mission.SALVATION.area, completion_critical=True),
},
SC2Campaign.EPILOGUE: {
SC2Mission.INTO_THE_VOID.mission_name: MissionInfo(SC2Mission.INTO_THE_VOID, [MissionConnection(25, SC2Campaign.WOL), MissionConnection(20, SC2Campaign.HOTS), MissionConnection(19, SC2Campaign.LOTV)], SC2Mission.INTO_THE_VOID.area, completion_critical=True),
SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name: MissionInfo(SC2Mission.THE_ESSENCE_OF_ETERNITY, [MissionConnection(1, SC2Campaign.EPILOGUE)], SC2Mission.THE_ESSENCE_OF_ETERNITY.area, completion_critical=True),
SC2Mission.AMON_S_FALL.mission_name: MissionInfo(SC2Mission.AMON_S_FALL, [MissionConnection(2, SC2Campaign.EPILOGUE)], SC2Mission.AMON_S_FALL.area, completion_critical=True),
},
SC2Campaign.NCO: {
SC2Mission.THE_ESCAPE.mission_name: MissionInfo(SC2Mission.THE_ESCAPE, [], SC2Mission.THE_ESCAPE.area, completion_critical=True),
SC2Mission.SUDDEN_STRIKE.mission_name: MissionInfo(SC2Mission.SUDDEN_STRIKE, [MissionConnection(1, SC2Campaign.NCO)], SC2Mission.SUDDEN_STRIKE.area, completion_critical=True),
SC2Mission.ENEMY_INTELLIGENCE.mission_name: MissionInfo(SC2Mission.ENEMY_INTELLIGENCE, [MissionConnection(2, SC2Campaign.NCO)], SC2Mission.ENEMY_INTELLIGENCE.area, completion_critical=True),
SC2Mission.TROUBLE_IN_PARADISE.mission_name: MissionInfo(SC2Mission.TROUBLE_IN_PARADISE, [MissionConnection(3, SC2Campaign.NCO)], SC2Mission.TROUBLE_IN_PARADISE.area, completion_critical=True),
SC2Mission.NIGHT_TERRORS.mission_name: MissionInfo(SC2Mission.NIGHT_TERRORS, [MissionConnection(4, SC2Campaign.NCO)], SC2Mission.NIGHT_TERRORS.area, completion_critical=True),
SC2Mission.FLASHPOINT.mission_name: MissionInfo(SC2Mission.FLASHPOINT, [MissionConnection(5, SC2Campaign.NCO)], SC2Mission.FLASHPOINT.area, completion_critical=True),
SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name: MissionInfo(SC2Mission.IN_THE_ENEMY_S_SHADOW, [MissionConnection(6, SC2Campaign.NCO)], SC2Mission.IN_THE_ENEMY_S_SHADOW.area, completion_critical=True),
SC2Mission.DARK_SKIES.mission_name: MissionInfo(SC2Mission.DARK_SKIES, [MissionConnection(7, SC2Campaign.NCO)], SC2Mission.DARK_SKIES.area, completion_critical=True),
SC2Mission.END_GAME.mission_name: MissionInfo(SC2Mission.END_GAME, [MissionConnection(8, SC2Campaign.NCO)], SC2Mission.END_GAME.area, completion_critical=True),
}
}
lookup_id_to_mission: Dict[int, SC2Mission] = {
mission.id: mission for mission in SC2Mission
}
lookup_name_to_mission: Dict[str, SC2Mission] = {
mission.mission_name: mission for mission in SC2Mission
}
lookup_id_to_campaign: Dict[int, SC2Campaign] = {
campaign.id: campaign for campaign in SC2Campaign
}
campaign_mission_table: Dict[SC2Campaign, Set[SC2Mission]] = {
campaign: set() for campaign in SC2Campaign
}
for mission in SC2Mission:
campaign_mission_table[mission.campaign].add(mission)
def get_campaign_difficulty(campaign: SC2Campaign, excluded_missions: Iterable[SC2Mission] = ()) -> MissionPools:
"""
:param campaign:
:param excluded_missions:
:return: Campaign's the most difficult non-excluded mission
"""
excluded_mission_set = set(excluded_missions)
included_missions = campaign_mission_table[campaign].difference(excluded_mission_set)
return max([mission.pool for mission in included_missions])
def get_campaign_goal_priority(campaign: SC2Campaign, excluded_missions: Iterable[SC2Mission] = ()) -> SC2CampaignGoalPriority:
"""
Gets a modified campaign goal priority.
If all the campaign's goal missions are excluded, it's ineligible to have the goal
If the campaign's very hard missions are excluded, the priority is lowered to hard
:param campaign:
:param excluded_missions:
:return:
"""
if excluded_missions is None:
return campaign.goal_priority
else:
goal_missions = set(get_campaign_potential_goal_missions(campaign))
excluded_mission_set = set(excluded_missions)
remaining_goals = goal_missions.difference(excluded_mission_set)
if remaining_goals == set():
# All potential goals are excluded, the campaign can't be a goal
return SC2CampaignGoalPriority.NONE
elif campaign.goal_priority == SC2CampaignGoalPriority.VERY_HARD:
# Check if a very hard campaign doesn't get rid of it's last very hard mission
difficulty = get_campaign_difficulty(campaign, excluded_missions)
if difficulty == MissionPools.VERY_HARD:
return SC2CampaignGoalPriority.VERY_HARD
else:
return SC2CampaignGoalPriority.HARD
else:
return campaign.goal_priority
class SC2CampaignGoal(NamedTuple):
mission: SC2Mission
location: str
campaign_final_mission_locations: Dict[SC2Campaign, SC2CampaignGoal] = {
SC2Campaign.WOL: SC2CampaignGoal(SC2Mission.ALL_IN, "All-In: Victory"),
SC2Campaign.PROPHECY: SC2CampaignGoal(SC2Mission.IN_UTTER_DARKNESS, "In Utter Darkness: Kills"),
SC2Campaign.HOTS: None,
SC2Campaign.PROLOGUE: SC2CampaignGoal(SC2Mission.EVIL_AWOKEN, "Evil Awoken: Victory"),
SC2Campaign.LOTV: SC2CampaignGoal(SC2Mission.SALVATION, "Salvation: Victory"),
SC2Campaign.EPILOGUE: None,
SC2Campaign.NCO: SC2CampaignGoal(SC2Mission.END_GAME, "End Game: Victory"),
}
campaign_alt_final_mission_locations: Dict[SC2Campaign, Dict[SC2Mission, str]] = {
SC2Campaign.WOL: {
SC2Mission.MAW_OF_THE_VOID: "Maw of the Void: Victory",
SC2Mission.ENGINE_OF_DESTRUCTION: "Engine of Destruction: Victory",
SC2Mission.SUPERNOVA: "Supernova: Victory",
SC2Mission.GATES_OF_HELL: "Gates of Hell: Victory",
SC2Mission.SHATTER_THE_SKY: "Shatter the Sky: Victory"
},
SC2Campaign.PROPHECY: None,
SC2Campaign.HOTS: {
SC2Mission.THE_RECKONING: "The Reckoning: Victory",
SC2Mission.THE_CRUCIBLE: "The Crucible: Victory",
SC2Mission.HAND_OF_DARKNESS: "Hand of Darkness: Victory",
SC2Mission.PHANTOMS_OF_THE_VOID: "Phantoms of the Void: Victory",
SC2Mission.PLANETFALL: "Planetfall: Victory",
SC2Mission.DEATH_FROM_ABOVE: "Death From Above: Victory"
},
SC2Campaign.PROLOGUE: {
SC2Mission.GHOSTS_IN_THE_FOG: "Ghosts in the Fog: Victory"
},
SC2Campaign.LOTV: {
SC2Mission.THE_HOST: "The Host: Victory",
SC2Mission.TEMPLAR_S_CHARGE: "Templar's Charge: Victory"
},
SC2Campaign.EPILOGUE: {
SC2Mission.AMON_S_FALL: "Amon's Fall: Victory",
SC2Mission.INTO_THE_VOID: "Into the Void: Victory",
SC2Mission.THE_ESSENCE_OF_ETERNITY: "The Essence of Eternity: Victory",
},
SC2Campaign.NCO: {
SC2Mission.FLASHPOINT: "Flashpoint: Victory",
SC2Mission.DARK_SKIES: "Dark Skies: Victory",
SC2Mission.NIGHT_TERRORS: "Night Terrors: Victory",
SC2Mission.TROUBLE_IN_PARADISE: "Trouble In Paradise: Victory"
}
}
campaign_race_exceptions: Dict[SC2Mission, SC2Race] = {
SC2Mission.WITH_FRIENDS_LIKE_THESE: SC2Race.TERRAN
}
def get_goal_location(mission: SC2Mission) -> Union[str, None]:
"""
:param mission:
:return: Goal location assigned to the goal mission
"""
campaign = mission.campaign
primary_campaign_goal = campaign_final_mission_locations[campaign]
if primary_campaign_goal is not None:
if primary_campaign_goal.mission == mission:
return primary_campaign_goal.location
campaign_alt_goals = campaign_alt_final_mission_locations[campaign]
if campaign_alt_goals is not None and mission in campaign_alt_goals:
return campaign_alt_goals.get(mission)
return mission.mission_name + ": Victory"
def get_campaign_potential_goal_missions(campaign: SC2Campaign) -> List[SC2Mission]:
"""
:param campaign:
:return: All missions that can be the campaign's goal
"""
missions: List[SC2Mission] = list()
primary_goal_mission = campaign_final_mission_locations[campaign]
if primary_goal_mission is not None:
missions.append(primary_goal_mission.mission)
alt_goal_locations = campaign_alt_final_mission_locations[campaign]
if alt_goal_locations is not None:
for mission in alt_goal_locations.keys():
missions.append(mission)
return missions
def get_no_build_missions() -> List[SC2Mission]:
return [mission for mission in SC2Mission if not mission.build]

View File

@@ -1,908 +0,0 @@
from dataclasses import dataclass, fields, Field
from typing import FrozenSet, Union, Set
from Options import Choice, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range, PerGameCommonOptions
from .MissionTables import SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_no_build_missions, \
campaign_mission_table
from worlds.AutoWorld import World
class GameDifficulty(Choice):
"""
The difficulty of the campaign, affects enemy AI, starting units, and game speed.
For those unfamiliar with the Archipelago randomizer, the recommended settings are one difficulty level
lower than the vanilla game
"""
display_name = "Game Difficulty"
option_casual = 0
option_normal = 1
option_hard = 2
option_brutal = 3
default = 1
class GameSpeed(Choice):
"""Optional setting to override difficulty-based game speed."""
display_name = "Game Speed"
option_default = 0
option_slower = 1
option_slow = 2
option_normal = 3
option_fast = 4
option_faster = 5
default = option_default
class DisableForcedCamera(Toggle):
"""
Prevents the game from moving or locking the camera without the player's consent.
"""
display_name = "Disable Forced Camera Movement"
class SkipCutscenes(Toggle):
"""
Skips all cutscenes and prevents dialog from blocking progress.
"""
display_name = "Skip Cutscenes"
class AllInMap(Choice):
"""Determines what version of All-In (WoL final map) that will be generated for the campaign."""
display_name = "All In Map"
option_ground = 0
option_air = 1
class MissionOrder(Choice):
"""
Determines the order the missions are played in. The last three mission orders end in a random mission.
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.
"""
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
class MaximumCampaignSize(Range):
"""
Sets an upper bound on how many missions to include when a variable-size mission order is selected.
If a set-size mission order is selected, does nothing.
"""
display_name = "Maximum Campaign Size"
range_start = 1
range_end = 83
default = 83
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.
"""
display_name = "Start with two unlocked missions on grid"
default = Toggle.option_false
class ColorChoice(Choice):
option_white = 0
option_red = 1
option_blue = 2
option_teal = 3
option_purple = 4
option_yellow = 5
option_orange = 6
option_green = 7
option_light_pink = 8
option_violet = 9
option_light_grey = 10
option_dark_green = 11
option_brown = 12
option_light_green = 13
option_dark_grey = 14
option_pink = 15
option_rainbow = 16
option_default = 17
default = option_default
class PlayerColorTerranRaynor(ColorChoice):
"""Determines in-game team color for playable Raynor's Raiders (Terran) factions."""
display_name = "Terran Player Color (Raynor)"
class PlayerColorProtoss(ColorChoice):
"""Determines in-game team color for playable Protoss factions."""
display_name = "Protoss Player Color"
class PlayerColorZerg(ColorChoice):
"""Determines in-game team color for playable Zerg factions before Kerrigan becomes Primal Kerrigan."""
display_name = "Zerg Player Color"
class PlayerColorZergPrimal(ColorChoice):
"""Determines in-game team color for playable Zerg factions after Kerrigan becomes Primal Kerrigan."""
display_name = "Zerg Player Color (Primal)"
class EnableWolMissions(DefaultOnToggle):
"""
Enables missions from main Wings of Liberty campaign.
"""
display_name = "Enable Wings of Liberty missions"
class EnableProphecyMissions(DefaultOnToggle):
"""
Enables missions from Prophecy mini-campaign.
"""
display_name = "Enable Prophecy missions"
class EnableHotsMissions(DefaultOnToggle):
"""
Enables missions from Heart of the Swarm campaign.
"""
display_name = "Enable Heart of the Swarm missions"
class EnableLotVPrologueMissions(DefaultOnToggle):
"""
Enables missions from Prologue campaign.
"""
display_name = "Enable Prologue (Legacy of the Void) missions"
class EnableLotVMissions(DefaultOnToggle):
"""
Enables missions from Legacy of the Void campaign.
"""
display_name = "Enable Legacy of the Void (main campaign) missions"
class EnableEpilogueMissions(DefaultOnToggle):
"""
Enables missions from Epilogue campaign.
These missions are considered very hard.
Enabling Wings of Liberty, Heart of the Swarm and Legacy of the Void is strongly recommended in order to play Epilogue.
Not recommended for short mission orders.
See also: Exclude Very Hard Missions
"""
display_name = "Enable Epilogue missions"
class EnableNCOMissions(DefaultOnToggle):
"""
Enables missions from Nova Covert Ops campaign.
Note: For best gameplay experience it's recommended to also enable Wings of Liberty campaign.
"""
display_name = "Enable Nova Covert Ops missions"
class ShuffleCampaigns(DefaultOnToggle):
"""
Shuffles the missions between campaigns if enabled.
Only available for Vanilla Shuffled and Mini Campaign mission order
"""
display_name = "Shuffle Campaigns"
class ShuffleNoBuild(DefaultOnToggle):
"""
Determines if the no-build missions are included in the shuffle.
If turned off, the no-build missions will not appear. Has no effect for Vanilla mission order.
"""
display_name = "Shuffle No-Build Missions"
class StarterUnit(Choice):
"""
Unlocks a random unit at the start of the game.
Off: No units are provided, the first unit must be obtained from the randomizer
Balanced: A unit that doesn't give the player too much power early on is given
Any Starter Unit: Any starter unit can be given
"""
display_name = "Starter Unit"
option_off = 0
option_balanced = 1
option_any_starter_unit = 2
class RequiredTactics(Choice):
"""
Determines the maximum tactical difficulty of the world (separate from mission difficulty). Higher settings
increase randomness.
Standard: All missions can be completed with good micro and macro.
Advanced: Completing missions may require relying on starting units and micro-heavy units.
No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!
Locks Grant Story Tech option to true.
"""
display_name = "Required Tactics"
option_standard = 0
option_advanced = 1
option_no_logic = 2
class GenericUpgradeMissions(Range):
"""Determines the percentage of missions in the mission order that must be completed before
level 1 of all weapon and armor upgrades is unlocked. Level 2 upgrades require double the amount of missions,
and level 3 requires triple the amount. The required amounts are always rounded down.
If set to 0, upgrades are instead added to the item pool and must be found to be used."""
display_name = "Generic Upgrade Missions"
range_start = 0
range_end = 100
default = 0
class GenericUpgradeResearch(Choice):
"""Determines how weapon and armor upgrades affect missions once unlocked.
Vanilla: Upgrades must be researched as normal.
Auto In No-Build: In No-Build missions, upgrades are automatically researched.
In all other missions, upgrades must be researched as normal.
Auto In Build: In No-Build missions, upgrades are unavailable as normal.
In all other missions, upgrades are automatically researched.
Always Auto: Upgrades are automatically researched in all missions."""
display_name = "Generic Upgrade Research"
option_vanilla = 0
option_auto_in_no_build = 1
option_auto_in_build = 2
option_always_auto = 3
class GenericUpgradeItems(Choice):
"""Determines how weapon and armor upgrades are split into items. All options produce 3 levels of each item.
Does nothing if upgrades are unlocked by completed mission counts.
Individual Items: All weapon and armor upgrades are each an item,
resulting in 18 total upgrade items for Terran and 15 total items for Zerg and Protoss each.
Bundle Weapon And Armor: All types of weapon upgrades are one item per race,
and all types of armor upgrades are one item per race,
resulting in 18 total items.
Bundle Unit Class: Weapon and armor upgrades are merged,
but upgrades are bundled separately for each race:
Infantry, Vehicle, and Starship upgrades for Terran (9 items),
Ground and Flyer upgrades for Zerg (6 items),
Ground and Air upgrades for Protoss (6 items),
resulting in 21 total items.
Bundle All: All weapon and armor upgrades are one item per race,
resulting in 9 total items."""
display_name = "Generic Upgrade Items"
option_individual_items = 0
option_bundle_weapon_and_armor = 1
option_bundle_unit_class = 2
option_bundle_all = 3
class NovaCovertOpsItems(Toggle):
"""
If turned on, the equipment upgrades from Nova Covert Ops may be present in the world.
If Nova Covert Ops campaign is enabled, this option is locked to be turned on.
"""
display_name = "Nova Covert Ops Items"
default = Toggle.option_true
class BroodWarItems(Toggle):
"""If turned on, returning items from StarCraft: Brood War may appear in the world."""
display_name = "Brood War Items"
default = Toggle.option_true
class ExtendedItems(Toggle):
"""If turned on, original items that did not appear in Campaign mode may appear in the world."""
display_name = "Extended Items"
default = Toggle.option_true
# Current maximum number of upgrades for a unit
MAX_UPGRADES_OPTION = 12
class EnsureGenericItems(Range):
"""
Specifies a minimum percentage of the generic item pool that will be present for the slot.
The generic item pool is the pool of all generically useful items after all exclusions.
Generically-useful items include: Worker upgrades, Building upgrades, economy upgrades,
Mercenaries, Kerrigan levels and abilities, and Spear of Adun abilities
Increasing this percentage will make units less common.
"""
display_name = "Ensure Generic Items"
range_start = 0
range_end = 100
default = 25
class MinNumberOfUpgrades(Range):
"""
Set a minimum to the number of upgrades a unit/structure can have.
Note that most units have 4 or 6 upgrades.
If a unit has fewer upgrades than the minimum, it will have all of its upgrades.
Doesn't affect shared unit upgrades.
"""
display_name = "Minimum number of upgrades per unit/structure"
range_start = 0
range_end = MAX_UPGRADES_OPTION
default = 2
class MaxNumberOfUpgrades(Range):
"""
Set a maximum to the number of upgrades a unit/structure can have. -1 is used to define unlimited.
Note that most unit have 4 to 6 upgrades.
Doesn't affect shared unit upgrades.
"""
display_name = "Maximum number of upgrades per unit/structure"
range_start = -1
range_end = MAX_UPGRADES_OPTION
default = -1
class KerriganPresence(Choice):
"""
Determines whether Kerrigan is playable outside of missions that require her.
Vanilla: Kerrigan is playable as normal, appears in the same missions as in vanilla game.
Not Present: Kerrigan is not playable, unless the mission requires her to be present. Other hero units stay playable,
and locations normally requiring Kerrigan can be checked by any unit.
Kerrigan level items, active abilities and passive abilities affecting her will not appear.
In missions where the Kerrigan unit is required, story abilities are given in same way as Grant Story Tech is set to true
Not Present And No Passives: In addition to the above, Kerrigan's passive abilities affecting other units (such as Twin Drones) will not appear.
Note: Always set to "Not Present" if Heart of the Swarm campaign is disabled.
"""
display_name = "Kerrigan Presence"
option_vanilla = 0
option_not_present = 1
option_not_present_and_no_passives = 2
class KerriganLevelsPerMissionCompleted(Range):
"""
Determines how many levels Kerrigan gains when a mission is beaten.
NOTE: Setting this too low can result in generation failures if The Infinite Cycle or Supreme are in the mission pool.
"""
display_name = "Levels Per Mission Beaten"
range_start = 0
range_end = 20
default = 0
class KerriganLevelsPerMissionCompletedCap(Range):
"""
Limits how many total levels Kerrigan can gain from beating missions. This does not affect levels gained from items.
Set to -1 to disable this limit.
NOTE: The following missions have these level requirements:
Supreme: 35
The Infinite Cycle: 70
See Grant Story Levels for more details.
"""
display_name = "Levels Per Mission Beaten Cap"
range_start = -1
range_end = 140
default = -1
class KerriganLevelItemSum(Range):
"""
Determines the sum of the level items in the world. This does not affect levels gained from beating missions.
NOTE: The following missions have these level requirements:
Supreme: 35
The Infinite Cycle: 70
See Grant Story Levels for more details.
"""
display_name = "Kerrigan Level Item Sum"
range_start = 0
range_end = 140
default = 70
class KerriganLevelItemDistribution(Choice):
"""Determines the amount and size of Kerrigan level items.
Vanilla: Uses the distribution in the vanilla campaign.
This entails 32 individual levels and 6 packs of varying sizes.
This distribution always adds up to 70, ignoring the Level Item Sum setting.
Smooth: Uses a custom, condensed distribution of 10 items between sizes 4 and 10,
intended to fit more levels into settings with little room for filler while keeping some variance in level gains.
This distribution always adds up to 70, ignoring the Level Item Sum setting.
Size 70: Uses items worth 70 levels each.
Size 35: Uses items worth 35 levels each.
Size 14: Uses items worth 14 levels each.
Size 10: Uses items worth 10 levels each.
Size 7: Uses items worth 7 levels each.
Size 5: Uses items worth 5 levels each.
Size 2: Uses items worth 2 level eachs.
Size 1: Uses individual levels. As there are not enough locations in the game for this distribution,
this will result in a greatly reduced total level, and is likely to remove many other items."""
display_name = "Kerrigan Level Item Distribution"
option_vanilla = 0
option_smooth = 1
option_size_70 = 2
option_size_35 = 3
option_size_14 = 4
option_size_10 = 5
option_size_7 = 6
option_size_5 = 7
option_size_2 = 8
option_size_1 = 9
default = option_smooth
class KerriganTotalLevelCap(Range):
"""
Limits how many total levels Kerrigan can gain from any source. Depending on your other settings,
there may be more levels available in the world, but they will not affect Kerrigan.
Set to -1 to disable this limit.
NOTE: The following missions have these level requirements:
Supreme: 35
The Infinite Cycle: 70
See Grant Story Levels for more details.
"""
display_name = "Total Level Cap"
range_start = -1
range_end = 140
default = -1
class StartPrimaryAbilities(Range):
"""Number of Primary Abilities (Kerrigan Tier 1, 2, and 4) to start the game with.
If set to 4, a Tier 7 ability is also included."""
display_name = "Starting Primary Abilities"
range_start = 0
range_end = 4
default = 0
class KerriganPrimalStatus(Choice):
"""Determines when Kerrigan appears in her Primal Zerg form.
This greatly increases her energy regeneration.
Vanilla: Kerrigan is human in missions that canonically appear before The Crucible,
and zerg thereafter.
Always Zerg: Kerrigan is always zerg.
Always Human: Kerrigan is always human.
Level 35: Kerrigan is human until reaching level 35, and zerg thereafter.
Half Completion: Kerrigan is human until half of the missions in the world are completed,
and zerg thereafter.
Item: Kerrigan's Primal Form is an item. She is human until it is found, and zerg thereafter."""
display_name = "Kerrigan Primal Status"
option_vanilla = 0
option_always_zerg = 1
option_always_human = 2
option_level_35 = 3
option_half_completion = 4
option_item = 5
class SpearOfAdunPresence(Choice):
"""
Determines in which missions Spear of Adun calldowns will be available.
Affects only abilities used from Spear of Adun top menu.
Not Present: Spear of Adun calldowns are unavailable.
LotV Protoss: Spear of Adun calldowns are only available in LotV main campaign
Protoss: Spear od Adun calldowns are available in any Protoss mission
Everywhere: Spear od Adun calldowns are available in any mission of any race
"""
display_name = "Spear of Adun Presence"
option_not_present = 0
option_lotv_protoss = 1
option_protoss = 2
option_everywhere = 3
default = option_lotv_protoss
# Fix case
@classmethod
def get_option_name(cls, value: int) -> str:
if value == SpearOfAdunPresence.option_lotv_protoss:
return "LotV Protoss"
else:
return super().get_option_name(value)
class SpearOfAdunPresentInNoBuild(Toggle):
"""
Determines if Spear of Adun calldowns are available in no-build missions.
If turned on, Spear of Adun calldown powers are available in missions specified under "Spear of Adun Presence".
If turned off, Spear of Adun calldown powers are unavailable in all no-build missions
"""
display_name = "Spear of Adun Present in No-Build"
class SpearOfAdunAutonomouslyCastAbilityPresence(Choice):
"""
Determines availability of Spear of Adun powers, that are autonomously cast.
Affects abilities like Reconstruction Beam or Overwatch
Not Presents: Autocasts are not available.
LotV Protoss: Spear of Adun autocasts are only available in LotV main campaign
Protoss: Spear od Adun autocasts are available in any Protoss mission
Everywhere: Spear od Adun autocasts are available in any mission of any race
"""
display_name = "Spear of Adun Autonomously Cast Powers Presence"
option_not_present = 0
option_lotv_protoss = 1
option_protoss = 2
option_everywhere = 3
default = option_lotv_protoss
# Fix case
@classmethod
def get_option_name(cls, value: int) -> str:
if value == SpearOfAdunPresence.option_lotv_protoss:
return "LotV Protoss"
else:
return super().get_option_name(value)
class SpearOfAdunAutonomouslyCastPresentInNoBuild(Toggle):
"""
Determines if Spear of Adun autocasts are available in no-build missions.
If turned on, Spear of Adun autocasts are available in missions specified under "Spear of Adun Autonomously Cast Powers Presence".
If turned off, Spear of Adun autocasts are unavailable in all no-build missions
"""
display_name = "Spear of Adun Autonomously Cast Powers Present in No-Build"
class GrantStoryTech(Toggle):
"""
If set true, grants special tech required for story mission completion for duration of the mission.
Otherwise, you need to find these tech by a normal means as items.
Affects story missions like Back in the Saddle and Supreme
Locked to true if Required Tactics is set to no logic.
"""
display_name = "Grant Story Tech"
class GrantStoryLevels(Choice):
"""
If enabled, grants Kerrigan the required minimum levels for the following missions:
Supreme: 35
The Infinite Cycle: 70
The bonus levels only apply during the listed missions, and can exceed the Total Level Cap.
If disabled, either of these missions is included, and there are not enough levels in the world, generation may fail.
To prevent this, either increase the amount of levels in the world, or enable this option.
If disabled and Required Tactics is set to no logic, this option is forced to Minimum.
Disabled: Kerrigan does not get bonus levels for these missions,
instead the levels must be gained from items or beating missions.
Additive: Kerrigan gains bonus levels equal to the mission's required level.
Minimum: Kerrigan is either at her real level, or at the mission's required level,
depending on which is higher.
"""
display_name = "Grant Story Levels"
option_disabled = 0
option_additive = 1
option_minimum = 2
default = option_minimum
class TakeOverAIAllies(Toggle):
"""
On maps supporting this feature allows you to take control over an AI Ally.
"""
display_name = "Take Over AI Allies"
class LockedItems(ItemSet):
"""Guarantees that these items will be unlockable"""
display_name = "Locked Items"
class ExcludedItems(ItemSet):
"""Guarantees that these items will not be unlockable"""
display_name = "Excluded Items"
class ExcludedMissions(OptionSet):
"""Guarantees that these missions will not appear in the campaign
Doesn't apply to vanilla mission order.
It may be impossible to build a valid campaign if too many missions are excluded."""
display_name = "Excluded Missions"
valid_keys = {mission.mission_name for mission in SC2Mission}
class ExcludeVeryHardMissions(Choice):
"""
Excludes Very Hard missions outside of Epilogue campaign (All-In, Salvation, and all Epilogue missions are considered Very Hard).
Doesn't apply to "Vanilla" mission order.
Default: Not excluded for mission orders "Vanilla Shuffled" or "Grid" with Maximum Campaign Size >= 20,
excluded for any other order
Yes: Non-Epilogue Very Hard missions are excluded and won't be generated
No: Non-Epilogue Very Hard missions can appear normally. Not recommended for too short mission orders.
See also: Excluded Missions, Enable Epilogue Missions, Maximum Campaign Size
"""
display_name = "Exclude Very Hard Missions"
option_default = 0
option_true = 1
option_false = 2
@classmethod
def get_option_name(cls, value):
return ["Default", "Yes", "No"][int(value)]
class LocationInclusion(Choice):
option_enabled = 0
option_resources = 1
option_disabled = 2
class VanillaLocations(LocationInclusion):
"""
Enables or disables item rewards for completing vanilla objectives.
Vanilla objectives are bonus objectives from the vanilla game,
along with some additional objectives to balance the missions.
Enable these locations for a balanced experience.
Enabled: All locations fitting into this do their normal rewards
Resources: Forces these locations to contain Starting Resources
Disabled: Removes item rewards from these locations.
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Vanilla Locations"
class ExtraLocations(LocationInclusion):
"""
Enables or disables item rewards for mission progress and minor objectives.
This includes mandatory mission objectives,
collecting reinforcements and resource pickups,
destroying structures, and overcoming minor challenges.
Enables these locations to add more checks and items to your world.
Enabled: All locations fitting into this do their normal rewards
Resources: Forces these locations to contain Starting Resources
Disabled: Removes item rewards from these locations.
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Extra Locations"
class ChallengeLocations(LocationInclusion):
"""
Enables or disables item rewards for completing challenge tasks.
Challenges are tasks that are more difficult than completing the mission, and are often based on achievements.
You might be required to visit the same mission later after getting stronger in order to finish these tasks.
Enable these locations to increase the difficulty of completing the multiworld.
Enabled: All locations fitting into this do their normal rewards
Resources: Forces these locations to contain Starting Resources
Disabled: Removes item rewards from these locations.
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Challenge Locations"
class MasteryLocations(LocationInclusion):
"""
Enables or disables item rewards for overcoming especially difficult challenges.
These challenges are often based on Mastery achievements and Feats of Strength.
Enable these locations to add the most difficult checks to the world.
Enabled: All locations fitting into this do their normal rewards
Resources: Forces these locations to contain Starting Resources
Disabled: Removes item rewards from these locations.
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Mastery Locations"
class MineralsPerItem(Range):
"""
Configures how many minerals are given per resource item.
"""
display_name = "Minerals Per Item"
range_start = 0
range_end = 500
default = 25
class VespenePerItem(Range):
"""
Configures how much vespene gas is given per resource item.
"""
display_name = "Vespene Per Item"
range_start = 0
range_end = 500
default = 25
class StartingSupplyPerItem(Range):
"""
Configures how much starting supply per is given per item.
"""
display_name = "Starting Supply Per Item"
range_start = 0
range_end = 200
default = 5
@dataclass
class Starcraft2Options(PerGameCommonOptions):
game_difficulty: GameDifficulty
game_speed: GameSpeed
disable_forced_camera: DisableForcedCamera
skip_cutscenes: SkipCutscenes
all_in_map: AllInMap
mission_order: MissionOrder
maximum_campaign_size: MaximumCampaignSize
grid_two_start_positions: GridTwoStartPositions
player_color_terran_raynor: PlayerColorTerranRaynor
player_color_protoss: PlayerColorProtoss
player_color_zerg: PlayerColorZerg
player_color_zerg_primal: PlayerColorZergPrimal
enable_wol_missions: EnableWolMissions
enable_prophecy_missions: EnableProphecyMissions
enable_hots_missions: EnableHotsMissions
enable_lotv_prologue_missions: EnableLotVPrologueMissions
enable_lotv_missions: EnableLotVMissions
enable_epilogue_missions: EnableEpilogueMissions
enable_nco_missions: EnableNCOMissions
shuffle_campaigns: ShuffleCampaigns
shuffle_no_build: ShuffleNoBuild
starter_unit: StarterUnit
required_tactics: RequiredTactics
ensure_generic_items: EnsureGenericItems
min_number_of_upgrades: MinNumberOfUpgrades
max_number_of_upgrades: MaxNumberOfUpgrades
generic_upgrade_missions: GenericUpgradeMissions
generic_upgrade_research: GenericUpgradeResearch
generic_upgrade_items: GenericUpgradeItems
kerrigan_presence: KerriganPresence
kerrigan_levels_per_mission_completed: KerriganLevelsPerMissionCompleted
kerrigan_levels_per_mission_completed_cap: KerriganLevelsPerMissionCompletedCap
kerrigan_level_item_sum: KerriganLevelItemSum
kerrigan_level_item_distribution: KerriganLevelItemDistribution
kerrigan_total_level_cap: KerriganTotalLevelCap
start_primary_abilities: StartPrimaryAbilities
kerrigan_primal_status: KerriganPrimalStatus
spear_of_adun_presence: SpearOfAdunPresence
spear_of_adun_present_in_no_build: SpearOfAdunPresentInNoBuild
spear_of_adun_autonomously_cast_ability_presence: SpearOfAdunAutonomouslyCastAbilityPresence
spear_of_adun_autonomously_cast_present_in_no_build: SpearOfAdunAutonomouslyCastPresentInNoBuild
grant_story_tech: GrantStoryTech
grant_story_levels: GrantStoryLevels
take_over_ai_allies: TakeOverAIAllies
locked_items: LockedItems
excluded_items: ExcludedItems
excluded_missions: ExcludedMissions
exclude_very_hard_missions: ExcludeVeryHardMissions
nco_items: NovaCovertOpsItems
bw_items: BroodWarItems
ext_items: ExtendedItems
vanilla_locations: VanillaLocations
extra_locations: ExtraLocations
challenge_locations: ChallengeLocations
mastery_locations: MasteryLocations
minerals_per_item: MineralsPerItem
vespene_per_item: VespenePerItem
starting_supply_per_item: StartingSupplyPerItem
def get_option_value(world: World, name: str) -> Union[int, FrozenSet]:
if world is None:
field: Field = [class_field for class_field in fields(Starcraft2Options) if class_field.name == name][0]
return field.type.default
player_option = getattr(world.options, name)
return player_option.value
def get_enabled_campaigns(world: World) -> Set[SC2Campaign]:
enabled_campaigns = set()
if get_option_value(world, "enable_wol_missions"):
enabled_campaigns.add(SC2Campaign.WOL)
if get_option_value(world, "enable_prophecy_missions"):
enabled_campaigns.add(SC2Campaign.PROPHECY)
if get_option_value(world, "enable_hots_missions"):
enabled_campaigns.add(SC2Campaign.HOTS)
if get_option_value(world, "enable_lotv_prologue_missions"):
enabled_campaigns.add(SC2Campaign.PROLOGUE)
if get_option_value(world, "enable_lotv_missions"):
enabled_campaigns.add(SC2Campaign.LOTV)
if get_option_value(world, "enable_epilogue_missions"):
enabled_campaigns.add(SC2Campaign.EPILOGUE)
if get_option_value(world, "enable_nco_missions"):
enabled_campaigns.add(SC2Campaign.NCO)
return enabled_campaigns
def get_disabled_campaigns(world: World) -> Set[SC2Campaign]:
all_campaigns = set(SC2Campaign)
enabled_campaigns = get_enabled_campaigns(world)
disabled_campaigns = all_campaigns.difference(enabled_campaigns)
disabled_campaigns.remove(SC2Campaign.GLOBAL)
return disabled_campaigns
def get_excluded_missions(world: World) -> Set[SC2Mission]:
mission_order_type = get_option_value(world, "mission_order")
excluded_mission_names = get_option_value(world, "excluded_missions")
shuffle_no_build = get_option_value(world, "shuffle_no_build")
disabled_campaigns = get_disabled_campaigns(world)
excluded_missions: Set[SC2Mission] = set([lookup_name_to_mission[name] for name in excluded_mission_names])
# Excluding Very Hard missions depending on options
if (get_option_value(world, "exclude_very_hard_missions") == ExcludeVeryHardMissions.option_true
) or (
get_option_value(world, "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 get_option_value(world, "maximum_campaign_size") < 20
)
)
):
excluded_missions = excluded_missions.union(
[mission for mission in SC2Mission if
mission.pool == MissionPools.VERY_HARD and mission.campaign != SC2Campaign.EPILOGUE]
)
# Omitting No-Build missions if not shuffling no-build
if not shuffle_no_build:
excluded_missions = excluded_missions.union(get_no_build_missions())
# Omitting missions not in enabled campaigns
for campaign in disabled_campaigns:
excluded_missions = excluded_missions.union(campaign_mission_table[campaign])
return excluded_missions
campaign_depending_orders = [
MissionOrder.option_vanilla,
MissionOrder.option_vanilla_shuffled,
MissionOrder.option_mini_campaign
]
kerrigan_unit_available = [
KerriganPresence.option_vanilla,
]

View File

@@ -1,661 +0,0 @@
from typing import Callable, Dict, List, Set, Union, Tuple, Optional
from BaseClasses import Item, Location
from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, progressive_if_nco, \
progressive_if_ext, spear_of_adun_calldowns, spear_of_adun_castable_passives, nova_equipment
from .MissionTables import mission_orders, MissionInfo, MissionPools, \
get_campaign_goal_priority, campaign_final_mission_locations, campaign_alt_final_mission_locations, \
SC2Campaign, SC2Race, SC2CampaignGoalPriority, SC2Mission
from .Options import get_option_value, MissionOrder, \
get_enabled_campaigns, get_disabled_campaigns, RequiredTactics, kerrigan_unit_available, GrantStoryTech, \
TakeOverAIAllies, SpearOfAdunPresence, SpearOfAdunAutonomouslyCastAbilityPresence, campaign_depending_orders, \
ShuffleCampaigns, get_excluded_missions, ShuffleNoBuild, ExtraLocations, GrantStoryLevels
from . import ItemNames
from worlds.AutoWorld import World
# Items with associated upgrades
UPGRADABLE_ITEMS = {item.parent_item for item in get_full_item_list().values() if item.parent_item}
BARRACKS_UNITS = {
ItemNames.MARINE, ItemNames.MEDIC, ItemNames.FIREBAT, ItemNames.MARAUDER,
ItemNames.REAPER, ItemNames.GHOST, ItemNames.SPECTRE, ItemNames.HERC,
}
FACTORY_UNITS = {
ItemNames.HELLION, ItemNames.VULTURE, ItemNames.GOLIATH, ItemNames.DIAMONDBACK,
ItemNames.SIEGE_TANK, ItemNames.THOR, ItemNames.PREDATOR, ItemNames.WIDOW_MINE,
ItemNames.CYCLONE, ItemNames.WARHOUND,
}
STARPORT_UNITS = {
ItemNames.MEDIVAC, ItemNames.WRAITH, ItemNames.VIKING, ItemNames.BANSHEE,
ItemNames.BATTLECRUISER, ItemNames.HERCULES, ItemNames.SCIENCE_VESSEL, ItemNames.RAVEN,
ItemNames.LIBERATOR, ItemNames.VALKYRIE,
}
def filter_missions(world: World) -> Dict[MissionPools, List[SC2Mission]]:
"""
Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets
"""
world: World = world
mission_order_type = get_option_value(world, "mission_order")
shuffle_no_build = get_option_value(world, "shuffle_no_build")
enabled_campaigns = get_enabled_campaigns(world)
grant_story_tech = get_option_value(world, "grant_story_tech") == GrantStoryTech.option_true
grant_story_levels = get_option_value(world, "grant_story_levels") != GrantStoryLevels.option_disabled
extra_locations = get_option_value(world, "extra_locations")
excluded_missions: Set[SC2Mission] = get_excluded_missions(world)
mission_pools: Dict[MissionPools, List[SC2Mission]] = {}
for mission in SC2Mission:
if not mission_pools.get(mission.pool):
mission_pools[mission.pool] = list()
mission_pools[mission.pool].append(mission)
# A bit of safeguard:
for mission_pool in MissionPools:
if not mission_pools.get(mission_pool):
mission_pools[mission_pool] = []
if mission_order_type == MissionOrder.option_vanilla:
# Vanilla uses the entire mission pool
goal_priorities: Dict[SC2Campaign, SC2CampaignGoalPriority] = {campaign: get_campaign_goal_priority(campaign) for campaign in enabled_campaigns}
goal_level = max(goal_priorities.values())
candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
candidate_campaigns.sort(key=lambda it: it.id)
goal_campaign = world.random.choice(candidate_campaigns)
if campaign_final_mission_locations[goal_campaign] is not None:
mission_pools[MissionPools.FINAL] = [campaign_final_mission_locations[goal_campaign].mission]
else:
mission_pools[MissionPools.FINAL] = [list(campaign_alt_final_mission_locations[goal_campaign].keys())[0]]
remove_final_mission_from_other_pools(mission_pools)
return mission_pools
# Finding the goal map
goal_mission: Optional[SC2Mission] = None
if mission_order_type in campaign_depending_orders:
# Prefer long campaigns over shorter ones and harder missions over easier ones
goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns}
goal_level = max(goal_priorities.values())
candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
candidate_campaigns.sort(key=lambda it: it.id)
goal_campaign = world.random.choice(candidate_campaigns)
primary_goal = campaign_final_mission_locations[goal_campaign]
if primary_goal is None or primary_goal.mission in excluded_missions:
# No primary goal or its mission is excluded
candidate_missions = list(campaign_alt_final_mission_locations[goal_campaign].keys())
candidate_missions = [mission for mission in candidate_missions if mission not in excluded_missions]
if len(candidate_missions) == 0:
raise Exception("There are no valid goal missions. Please exclude fewer missions.")
goal_mission = world.random.choice(candidate_missions)
else:
goal_mission = primary_goal.mission
else:
# Find one of the missions with the hardest difficulty
available_missions: List[SC2Mission] = \
[mission for mission in SC2Mission
if (mission not in excluded_missions and mission.campaign in enabled_campaigns)]
available_missions.sort(key=lambda it: it.id)
# Loop over pools, from hardest to easiest
for mission_pool in range(MissionPools.VERY_HARD, MissionPools.STARTER - 1, -1):
pool_missions: List[SC2Mission] = [mission for mission in available_missions if mission.pool == mission_pool]
if pool_missions:
goal_mission = world.random.choice(pool_missions)
break
if goal_mission is None:
raise Exception("There are no valid goal missions. Please exclude fewer missions.")
# Excluding missions
for difficulty, mission_pool in mission_pools.items():
mission_pools[difficulty] = [mission for mission in mission_pool if mission not in excluded_missions]
mission_pools[MissionPools.FINAL] = [goal_mission]
# Mission pool changes
adv_tactics = get_option_value(world, "required_tactics") != RequiredTactics.option_standard
def move_mission(mission: SC2Mission, current_pool, new_pool):
if mission in mission_pools[current_pool]:
mission_pools[current_pool].remove(mission)
mission_pools[new_pool].append(mission)
# WoL
if shuffle_no_build == ShuffleNoBuild.option_false or adv_tactics:
# Replacing No Build missions with Easy missions
# WoL
move_mission(SC2Mission.ZERO_HOUR, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.EVACUATION, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.DEVILS_PLAYGROUND, MissionPools.EASY, MissionPools.STARTER)
# LotV
move_mission(SC2Mission.THE_GROWING_SHADOW, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.THE_SPEAR_OF_ADUN, MissionPools.EASY, MissionPools.STARTER)
if extra_locations == ExtraLocations.option_enabled:
move_mission(SC2Mission.SKY_SHIELD, MissionPools.EASY, MissionPools.STARTER)
# Pushing this to Easy
move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY, MissionPools.MEDIUM, MissionPools.EASY)
if shuffle_no_build == ShuffleNoBuild.option_false:
# Pushing Outbreak to Normal, as it cannot be placed as the second mission on Build-Only
move_mission(SC2Mission.OUTBREAK, MissionPools.EASY, MissionPools.MEDIUM)
# Pushing extra Normal missions to Easy
move_mission(SC2Mission.ECHOES_OF_THE_FUTURE, MissionPools.MEDIUM, MissionPools.EASY)
move_mission(SC2Mission.CUTTHROAT, MissionPools.MEDIUM, MissionPools.EASY)
# Additional changes on Advanced Tactics
if adv_tactics:
# WoL
move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.SMASH_AND_GRAB, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.THE_MOEBIUS_FACTOR, MissionPools.MEDIUM, MissionPools.EASY)
move_mission(SC2Mission.WELCOME_TO_THE_JUNGLE, MissionPools.MEDIUM, MissionPools.EASY)
move_mission(SC2Mission.ENGINE_OF_DESTRUCTION, MissionPools.HARD, MissionPools.MEDIUM)
# LotV
move_mission(SC2Mission.AMON_S_REACH, MissionPools.EASY, MissionPools.STARTER)
# Prophecy needs to be adjusted on tiny grid
if enabled_campaigns == {SC2Campaign.PROPHECY} and mission_order_type == MissionOrder.option_tiny_grid:
move_mission(SC2Mission.A_SINISTER_TURN, MissionPools.MEDIUM, MissionPools.EASY)
# Prologue's only valid starter is the goal mission
if enabled_campaigns == {SC2Campaign.PROLOGUE} \
or mission_order_type in campaign_depending_orders \
and get_option_value(world, "shuffle_campaigns") == ShuffleCampaigns.option_false:
move_mission(SC2Mission.DARK_WHISPERS, MissionPools.EASY, MissionPools.STARTER)
# HotS
kerriganless = get_option_value(world, "kerrigan_presence") not in kerrigan_unit_available \
or SC2Campaign.HOTS not in enabled_campaigns
if adv_tactics:
# Medium -> Easy
for mission in (SC2Mission.FIRE_IN_THE_SKY, SC2Mission.WAKING_THE_ANCIENT, SC2Mission.CONVICTION):
move_mission(mission, MissionPools.MEDIUM, MissionPools.EASY)
# Hard -> Medium
move_mission(SC2Mission.PHANTOMS_OF_THE_VOID, MissionPools.HARD, MissionPools.MEDIUM)
if not kerriganless:
# Additional starter mission assuming player starts with minimal anti-air
move_mission(SC2Mission.WAKING_THE_ANCIENT, MissionPools.EASY, MissionPools.STARTER)
if grant_story_tech:
# Additional starter mission if player is granted story tech
move_mission(SC2Mission.ENEMY_WITHIN, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.TEMPLAR_S_RETURN, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.THE_ESCAPE, MissionPools.MEDIUM, MissionPools.STARTER)
move_mission(SC2Mission.IN_THE_ENEMY_S_SHADOW, MissionPools.MEDIUM, MissionPools.STARTER)
if (grant_story_tech and grant_story_levels) or kerriganless:
# The player has, all the stuff he needs, provided under these settings
move_mission(SC2Mission.SUPREME, MissionPools.MEDIUM, MissionPools.STARTER)
move_mission(SC2Mission.THE_INFINITE_CYCLE, MissionPools.HARD, MissionPools.STARTER)
if get_option_value(world, "take_over_ai_allies") == TakeOverAIAllies.option_true:
move_mission(SC2Mission.HARBINGER_OF_OBLIVION, MissionPools.MEDIUM, MissionPools.STARTER)
if len(mission_pools[MissionPools.STARTER]) < 2 and not kerriganless or adv_tactics:
# Conditionally moving Easy missions to Starter
move_mission(SC2Mission.HARVEST_OF_SCREAMS, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.DOMINATION, MissionPools.EASY, MissionPools.STARTER)
if len(mission_pools[MissionPools.STARTER]) < 2:
move_mission(SC2Mission.TEMPLAR_S_RETURN, MissionPools.EASY, MissionPools.STARTER)
if len(mission_pools[MissionPools.STARTER]) + len(mission_pools[MissionPools.EASY]) < 2:
# Flashpoint needs just a few items at start but competent comp at the end
move_mission(SC2Mission.FLASHPOINT, MissionPools.HARD, MissionPools.EASY)
remove_final_mission_from_other_pools(mission_pools)
return mission_pools
def remove_final_mission_from_other_pools(mission_pools: Dict[MissionPools, List[SC2Mission]]):
final_missions = mission_pools[MissionPools.FINAL]
for pool, missions in mission_pools.items():
if pool == MissionPools.FINAL:
continue
for final_mission in final_missions:
while final_mission in missions:
missions.remove(final_mission)
def get_item_upgrades(inventory: List[Item], parent_item: Union[Item, str]) -> List[Item]:
item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
return [
inv_item for inv_item in inventory
if get_full_item_list()[inv_item.name].parent_item == item_name
]
def get_item_quantity(item: Item, world: World):
if (not get_option_value(world, "nco_items")) \
and SC2Campaign.NCO in get_disabled_campaigns(world) \
and item.name in progressive_if_nco:
return 1
if (not get_option_value(world, "ext_items")) \
and item.name in progressive_if_ext:
return 1
return get_full_item_list()[item.name].quantity
def copy_item(item: Item):
return Item(item.name, item.classification, item.code, item.player)
def num_missions(world: World) -> int:
mission_order_type = get_option_value(world, "mission_order")
if mission_order_type != MissionOrder.option_grid:
mission_order = mission_orders[mission_order_type]()
misssions = [mission for campaign in mission_order for mission in mission_order[campaign]]
return len(misssions) - 1 # Menu
else:
mission_pools = filter_missions(world)
return sum(len(pool) for _, pool in mission_pools.items())
class ValidInventory:
def has(self, item: str, player: int):
return item in self.logical_inventory
def has_any(self, items: Set[str], player: int):
return any(item in self.logical_inventory for item in items)
def has_all(self, items: Set[str], player: int):
return all(item in self.logical_inventory for item in items)
def has_group(self, item_group: str, player: int, count: int = 1):
return False # Deliberately fails here, as item pooling is not aware about mission layout
def count_group(self, item_name_group: str, player: int) -> int:
return 0 # For item filtering assume no missions are beaten
def count(self, item: str, player: int) -> int:
return len([inventory_item for inventory_item in self.logical_inventory if inventory_item == item])
def has_units_per_structure(self) -> bool:
return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure
def generate_reduced_inventory(self, inventory_size: int, mission_requirements: List[Tuple[str, Callable]]) -> List[Item]:
"""Attempts to generate a reduced inventory that can fulfill the mission requirements."""
inventory: List[Item] = list(self.item_pool)
locked_items: List[Item] = list(self.locked_items)
item_list = get_full_item_list()
self.logical_inventory = [
item.name for item in inventory + locked_items + self.existing_items
if item_list[item.name].is_important_for_filtering() # Track all Progression items and those with complex rules for filtering
]
requirements = mission_requirements
parent_items = self.item_children.keys()
parent_lookup = {child: parent for parent, children in self.item_children.items() for child in children}
minimum_upgrades = get_option_value(self.world, "min_number_of_upgrades")
def attempt_removal(item: Item) -> bool:
inventory.remove(item)
# Only run logic checks when removing logic items
if item.name in self.logical_inventory:
self.logical_inventory.remove(item.name)
if not all(requirement(self) for (_, requirement) in mission_requirements):
# If item cannot be removed, lock or revert
self.logical_inventory.append(item.name)
for _ in range(get_item_quantity(item, self.world)):
locked_items.append(copy_item(item))
return False
return True
# Limit the maximum number of upgrades
maxNbUpgrade = get_option_value(self.world, "max_number_of_upgrades")
if maxNbUpgrade != -1:
unit_avail_upgrades = {}
# Needed to take into account locked/existing items
unit_nb_upgrades = {}
for item in inventory:
cItem = item_list[item.name]
if item.name in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
unit_avail_upgrades[item.name] = []
unit_nb_upgrades[item.name] = 0
elif cItem.parent_item is not None:
if cItem.parent_item not in unit_avail_upgrades:
unit_avail_upgrades[cItem.parent_item] = [item]
unit_nb_upgrades[cItem.parent_item] = 1
else:
unit_avail_upgrades[cItem.parent_item].append(item)
unit_nb_upgrades[cItem.parent_item] += 1
# For those two categories, we count them but dont include them in removal
for item in locked_items + self.existing_items:
cItem = item_list[item.name]
if item.name in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
unit_avail_upgrades[item.name] = []
unit_nb_upgrades[item.name] = 0
elif cItem.parent_item is not None:
if cItem.parent_item not in unit_avail_upgrades:
unit_nb_upgrades[cItem.parent_item] = 1
else:
unit_nb_upgrades[cItem.parent_item] += 1
# Making sure that the upgrades being removed is random
shuffled_unit_upgrade_list = list(unit_avail_upgrades.keys())
self.world.random.shuffle(shuffled_unit_upgrade_list)
for unit in shuffled_unit_upgrade_list:
while (unit_nb_upgrades[unit] > maxNbUpgrade) \
and (len(unit_avail_upgrades[unit]) > 0):
itemCandidate = self.world.random.choice(unit_avail_upgrades[unit])
success = attempt_removal(itemCandidate)
# Whatever it succeed to remove the iventory or it fails and thus
# lock it, the upgrade is no longer available for removal
unit_avail_upgrades[unit].remove(itemCandidate)
if success:
unit_nb_upgrades[unit] -= 1
# Locking minimum upgrades for items that have already been locked/placed when minimum required
if minimum_upgrades > 0:
known_items = self.existing_items + locked_items
known_parents = [item for item in known_items if item in parent_items]
for parent in known_parents:
child_items = self.item_children[parent]
removable_upgrades = [item for item in inventory if item in child_items]
locked_upgrade_count = sum(1 if item in child_items else 0 for item in known_items)
self.world.random.shuffle(removable_upgrades)
while len(removable_upgrades) > 0 and locked_upgrade_count < minimum_upgrades:
item_to_lock = removable_upgrades.pop()
inventory.remove(item_to_lock)
locked_items.append(copy_item(item_to_lock))
locked_upgrade_count += 1
if self.min_units_per_structure > 0 and self.has_units_per_structure():
requirements.append(("Minimum units per structure", lambda state: state.has_units_per_structure()))
# Determining if the full-size inventory can complete campaign
failed_locations: List[str] = [location for (location, requirement) in requirements if not requirement(self)]
if len(failed_locations) > 0:
raise Exception(f"Too many items excluded - couldn't satisfy access rules for the following locations:\n{failed_locations}")
# Optionally locking generic items
generic_items = [item for item in inventory if item.name in second_pass_placeable_items]
reserved_generic_percent = get_option_value(self.world, "ensure_generic_items") / 100
reserved_generic_amount = int(len(generic_items) * reserved_generic_percent)
removable_generic_items = []
self.world.random.shuffle(generic_items)
for item in generic_items[:reserved_generic_amount]:
locked_items.append(copy_item(item))
inventory.remove(item)
if item.name not in self.logical_inventory and item.name not in self.locked_items:
removable_generic_items.append(item)
# Main cull process
unused_items: List[str] = [] # Reusable items for the second pass
while len(inventory) + len(locked_items) > inventory_size:
if len(inventory) == 0:
# There are more items than locations and all of them are already locked due to YAML or logic.
# First, drop non-logic generic items to free up space
while len(removable_generic_items) > 0 and len(locked_items) > inventory_size:
removed_item = removable_generic_items.pop()
locked_items.remove(removed_item)
# If there still isn't enough space, push locked items into start inventory
self.world.random.shuffle(locked_items)
while len(locked_items) > inventory_size:
item: Item = locked_items.pop()
self.multiworld.push_precollected(item)
break
# Select random item from removable items
item = self.world.random.choice(inventory)
# Do not remove item if it would drop upgrades below minimum
if minimum_upgrades > 0:
parent_item = parent_lookup.get(item, None)
if parent_item:
count = sum(1 if item in self.item_children[parent_item] else 0 for item in inventory + locked_items)
if count <= minimum_upgrades:
if parent_item in inventory:
# Attempt to remove parent instead, if possible
item = parent_item
else:
# Lock remaining upgrades
for item in self.item_children[parent_item]:
if item in inventory:
inventory.remove(item)
locked_items.append(copy_item(item))
continue
# Drop child items when removing a parent
if item in parent_items:
items_to_remove = [item for item in self.item_children[item] if item in inventory]
success = attempt_removal(item)
if success:
while len(items_to_remove) > 0:
item_to_remove = items_to_remove.pop()
if item_to_remove not in inventory:
continue
attempt_removal(item_to_remove)
else:
# Unimportant upgrades may be added again in the second pass
if attempt_removal(item):
unused_items.append(item.name)
pool_items: List[str] = [item.name for item in (inventory + locked_items + self.existing_items)]
unused_items = [
unused_item for unused_item in unused_items
if item_list[unused_item].parent_item is None
or item_list[unused_item].parent_item in pool_items
]
# Removing extra dependencies
# WoL
logical_inventory_set = set(self.logical_inventory)
if not spider_mine_sources & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Spider Mine)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Spider Mine)")]
if not BARRACKS_UNITS & logical_inventory_set:
inventory = [
item for item in inventory
if not (item.name.startswith(ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX)
or item.name == ItemNames.ORBITAL_STRIKE)]
unused_items = [
item_name for item_name in unused_items
if not (item_name.startswith(
ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX)
or item_name == ItemNames.ORBITAL_STRIKE)]
if not FACTORY_UNITS & logical_inventory_set:
inventory = [item for item in inventory if not item.name.startswith(ItemNames.TERRAN_VEHICLE_UPGRADE_PREFIX)]
unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.TERRAN_VEHICLE_UPGRADE_PREFIX)]
if not STARPORT_UNITS & logical_inventory_set:
inventory = [item for item in inventory if not item.name.startswith(ItemNames.TERRAN_SHIP_UPGRADE_PREFIX)]
unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.TERRAN_SHIP_UPGRADE_PREFIX)]
# HotS
# Baneling without sources => remove Baneling and upgrades
if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory
and ItemNames.ZERGLING not in self.logical_inventory
and ItemNames.KERRIGAN_SPAWN_BANELINGS not in self.logical_inventory
):
inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT]
# Spawn Banelings without Zergling => remove Baneling unit, keep upgrades except macro ones
if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory
and ItemNames.ZERGLING not in self.logical_inventory
and ItemNames.KERRIGAN_SPAWN_BANELINGS in self.logical_inventory
):
inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT]
inventory = [item for item in inventory if item.name != ItemNames.BANELING_RAPID_METAMORPH]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.BANELING_RAPID_METAMORPH]
if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR, ItemNames.SCOURGE} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
locked_items = [item for item in locked_items if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
# T3 items removal rules - remove morph and its upgrades if the basic unit isn't in
if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Mutalisk/Corruptor)")]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Mutalisk/Corruptor)")]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT]
if ItemNames.ROACH not in logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.ROACH_RAVAGER_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ROACH_RAVAGER_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT]
if ItemNames.HYDRALISK not in logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Hydralisk)")]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_IMPALER_ASPECT]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Hydralisk)")]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.HYDRALISK_IMPALER_ASPECT]
# LotV
# Shared unit upgrades between several units
if not {ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Stalker/Instigator/Slayer)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Stalker/Instigator/Slayer)")]
if not {ItemNames.PHOENIX, ItemNames.MIRAGE} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Phoenix/Mirage)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Phoenix/Mirage)")]
if not {ItemNames.VOID_RAY, ItemNames.DESTROYER} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Void Ray/Destroyer)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Void Ray/Destroyer)")]
if not {ItemNames.IMMORTAL, ItemNames.ANNIHILATOR} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Immortal/Annihilator)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Immortal/Annihilator)")]
if not {ItemNames.DARK_TEMPLAR, ItemNames.AVENGER, ItemNames.BLOOD_HUNTER} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Dark Templar/Avenger/Blood Hunter)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Dark Templar/Avenger/Blood Hunter)")]
if not {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ASCENDANT, ItemNames.DARK_TEMPLAR} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Archon)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Archon)")]
logical_inventory_set.difference_update([item_name for item_name in logical_inventory_set if item_name.endswith("(Archon)")])
if not {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ARCHON_HIGH_ARCHON} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(High Templar/Signifier)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(High Templar/Signifier)")]
if ItemNames.SUPPLICANT not in logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.ASCENDANT_POWER_OVERWHELMING]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ASCENDANT_POWER_OVERWHELMING]
if not {ItemNames.DARK_ARCHON, ItemNames.DARK_TEMPLAR_DARK_ARCHON_MELD} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Dark Archon)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Dark Archon)")]
if not {ItemNames.SENTRY, ItemNames.ENERGIZER, ItemNames.HAVOC} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Sentry/Energizer/Havoc)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Sentry/Energizer/Havoc)")]
if not {ItemNames.SENTRY, ItemNames.ENERGIZER, ItemNames.HAVOC, ItemNames.SHIELD_BATTERY} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Sentry/Energizer/Havoc/Shield Battery)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Sentry/Energizer/Havoc/Shield Battery)")]
if not {ItemNames.ZEALOT, ItemNames.CENTURION, ItemNames.SENTINEL} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Zealot/Sentinel/Centurion)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Zealot/Sentinel/Centurion)")]
# Static defense upgrades only if static defense present
if not {ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH, ItemNames.NEXUS_OVERCHARGE, ItemNames.SHIELD_BATTERY} & logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.ENHANCED_TARGETING]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ENHANCED_TARGETING]
if not {ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH, ItemNames.NEXUS_OVERCHARGE} & logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.OPTIMIZED_ORDNANCE]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.OPTIMIZED_ORDNANCE]
# Cull finished, adding locked items back into inventory
inventory += locked_items
# Replacing empty space with generically useful items
replacement_items = [item for item in self.item_pool
if (item not in inventory
and item not in self.locked_items
and (
item.name in second_pass_placeable_items
or item.name in unused_items))]
self.world.random.shuffle(replacement_items)
while len(inventory) < inventory_size and len(replacement_items) > 0:
item = replacement_items.pop()
inventory.append(item)
return inventory
def __init__(self, world: World ,
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item],
used_races: Set[SC2Race], nova_equipment_used: bool):
self.multiworld = world.multiworld
self.player = world.player
self.world: World = world
self.logical_inventory = list()
self.locked_items = locked_items[:]
self.existing_items = existing_items
soa_presence = get_option_value(world, "spear_of_adun_presence")
soa_autocast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence")
# Initial filter of item pool
self.item_pool = []
item_quantities: dict[str, int] = dict()
# Inventory restrictiveness based on number of missions with checks
mission_count = num_missions(world)
self.min_units_per_structure = int(mission_count / 7)
min_upgrades = 1 if mission_count < 10 else 2
for item in item_pool:
item_info = get_full_item_list()[item.name]
if item_info.race != SC2Race.ANY and item_info.race not in used_races:
if soa_presence == SpearOfAdunPresence.option_everywhere \
and item.name in spear_of_adun_calldowns:
# Add SoA powers regardless of used races as it's present everywhere
self.item_pool.append(item)
if soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_everywhere \
and item.name in spear_of_adun_castable_passives:
self.item_pool.append(item)
# Drop any item belonging to a race not used in the campaign
continue
if item.name in nova_equipment and not nova_equipment_used:
# Drop Nova equipment if there's no NCO mission generated
continue
if item_info.type == "Upgrade":
# Locking upgrades based on mission duration
if item.name not in item_quantities:
item_quantities[item.name] = 0
item_quantities[item.name] += 1
if item_quantities[item.name] <= min_upgrades:
self.locked_items.append(item)
else:
self.item_pool.append(item)
elif item_info.type == "Goal":
self.locked_items.append(item)
else:
self.item_pool.append(item)
self.item_children: Dict[Item, List[Item]] = dict()
for item in self.item_pool + locked_items + existing_items:
if item.name in UPGRADABLE_ITEMS:
self.item_children[item] = get_item_upgrades(self.item_pool, item)
def filter_items(world: World, mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]], location_cache: List[Location],
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item]) -> List[Item]:
"""
Returns a semi-randomly pruned set of items based on number of available locations.
The returned inventory must be capable of logically accessing every location in the world.
"""
open_locations = [location for location in location_cache if location.item is None]
inventory_size = len(open_locations)
used_races = get_used_races(mission_req_table, world)
nova_equipment_used = is_nova_equipment_used(mission_req_table)
mission_requirements = [(location.name, location.access_rule) for location in location_cache]
valid_inventory = ValidInventory(world, item_pool, existing_items, locked_items, used_races, nova_equipment_used)
valid_items = valid_inventory.generate_reduced_inventory(inventory_size, mission_requirements)
return valid_items
def get_used_races(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]], world: World) -> Set[SC2Race]:
grant_story_tech = get_option_value(world, "grant_story_tech")
take_over_ai_allies = get_option_value(world, "take_over_ai_allies")
kerrigan_presence = get_option_value(world, "kerrigan_presence") in kerrigan_unit_available \
and SC2Campaign.HOTS in get_enabled_campaigns(world)
missions = missions_in_mission_table(mission_req_table)
# By missions
races = set([mission.race for mission in missions])
# Conditionally logic-less no-builds (They're set to SC2Race.ANY):
if grant_story_tech == GrantStoryTech.option_false:
if SC2Mission.ENEMY_WITHIN in missions:
# Zerg units need to be unlocked
races.add(SC2Race.ZERG)
if kerrigan_presence \
and not missions.isdisjoint({SC2Mission.BACK_IN_THE_SADDLE, SC2Mission.SUPREME, SC2Mission.CONVICTION, SC2Mission.THE_INFINITE_CYCLE}):
# You need some Kerrigan abilities (they're granted if Kerriganless or story tech granted)
races.add(SC2Race.ZERG)
# If you take over the AI Ally, you need to have its race stuff
if take_over_ai_allies == TakeOverAIAllies.option_true \
and not missions.isdisjoint({SC2Mission.THE_RECKONING}):
# Jimmy in The Reckoning
races.add(SC2Race.TERRAN)
return races
def is_nova_equipment_used(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]]) -> bool:
missions = missions_in_mission_table(mission_req_table)
return any([mission.campaign == SC2Campaign.NCO for mission in missions])
def missions_in_mission_table(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]]) -> Set[SC2Mission]:
return set([mission.mission for campaign_missions in mission_req_table.values() for mission in
campaign_missions.values()])

View File

@@ -1,691 +0,0 @@
from typing import List, Dict, Tuple, Optional, Callable, NamedTuple, Union
import math
from BaseClasses import MultiWorld, 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
from .PoolFilter import filter_missions
from worlds.AutoWorld import World
class SC2MissionSlot(NamedTuple):
campaign: SC2Campaign
slot: Union[MissionPools, SC2Mission, None]
def create_regions(
world: World, locations: Tuple[LocationData, ...], location_cache: List[Location]
) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
"""
Creates region connections by calling the multiworld's `connect()` methods
Returns a 3-tuple containing:
* dict[SC2Campaign, Dict[str, MissionInfo]] mapping a campaign and mission name to its data
* 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")
if mission_order_type == MissionOrder.option_vanilla:
return create_vanilla_regions(world, locations, location_cache)
elif mission_order_type == MissionOrder.option_grid:
return create_grid_regions(world, locations, location_cache)
else:
return create_structured_regions(world, locations, location_cache, mission_order_type)
def create_vanilla_regions(
world: World,
locations: Tuple[LocationData, ...],
location_cache: List[Location],
) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
locations_per_region = get_locations_per_region(locations)
regions = [create_region(world, locations_per_region, location_cache, "Menu")]
mission_pools: Dict[MissionPools, List[SC2Mission]] = filter_missions(world)
final_mission = mission_pools[MissionPools.FINAL][0]
enabled_campaigns = get_enabled_campaigns(world)
names: Dict[str, int] = {}
# Generating all regions and locations for each enabled campaign
for campaign in sorted(enabled_campaigns):
for region_name in vanilla_mission_req_table[campaign].keys():
regions.append(create_region(world, locations_per_region, location_cache, region_name))
world.multiworld.regions += regions
vanilla_mission_reqs = {campaign: missions for campaign, missions in vanilla_mission_req_table.items() if campaign in enabled_campaigns}
def wol_cleared_missions(state: CollectionState, mission_count: int) -> bool:
return state.has_group("WoL Missions", world.player, mission_count)
player: int = world.player
if SC2Campaign.WOL in enabled_campaigns:
connect(world, names, 'Menu', 'Liberation Day')
connect(world, names, 'Liberation Day', 'The Outlaws',
lambda state: state.has("Beat Liberation Day", player))
connect(world, names, 'The Outlaws', 'Zero Hour',
lambda state: state.has("Beat The Outlaws", player))
connect(world, names, 'Zero Hour', 'Evacuation',
lambda state: state.has("Beat Zero Hour", player))
connect(world, names, 'Evacuation', 'Outbreak',
lambda state: state.has("Beat Evacuation", player))
connect(world, names, "Outbreak", "Safe Haven",
lambda state: wol_cleared_missions(state, 7) and state.has("Beat Outbreak", player))
connect(world, names, "Outbreak", "Haven's Fall",
lambda state: wol_cleared_missions(state, 7) and state.has("Beat Outbreak", player))
connect(world, names, 'Zero Hour', 'Smash and Grab',
lambda state: state.has("Beat Zero Hour", player))
connect(world, names, 'Smash and Grab', 'The Dig',
lambda state: wol_cleared_missions(state, 8) and state.has("Beat Smash and Grab", player))
connect(world, names, 'The Dig', 'The Moebius Factor',
lambda state: wol_cleared_missions(state, 11) and state.has("Beat The Dig", player))
connect(world, names, 'The Moebius Factor', 'Supernova',
lambda state: wol_cleared_missions(state, 14) and state.has("Beat The Moebius Factor", player))
connect(world, names, 'Supernova', 'Maw of the Void',
lambda state: state.has("Beat Supernova", player))
connect(world, names, 'Zero Hour', "Devil's Playground",
lambda state: wol_cleared_missions(state, 4) and state.has("Beat Zero Hour", player))
connect(world, names, "Devil's Playground", 'Welcome to the Jungle',
lambda state: state.has("Beat Devil's Playground", player))
connect(world, names, "Welcome to the Jungle", 'Breakout',
lambda state: wol_cleared_missions(state, 8) and state.has("Beat Welcome to the Jungle", player))
connect(world, names, "Welcome to the Jungle", 'Ghost of a Chance',
lambda state: wol_cleared_missions(state, 8) and state.has("Beat Welcome to the Jungle", player))
connect(world, names, "Zero Hour", 'The Great Train Robbery',
lambda state: wol_cleared_missions(state, 6) and state.has("Beat Zero Hour", player))
connect(world, names, 'The Great Train Robbery', 'Cutthroat',
lambda state: state.has("Beat The Great Train Robbery", player))
connect(world, names, 'Cutthroat', 'Engine of Destruction',
lambda state: state.has("Beat Cutthroat", player))
connect(world, names, 'Engine of Destruction', 'Media Blitz',
lambda state: state.has("Beat Engine of Destruction", player))
connect(world, names, 'Media Blitz', 'Piercing the Shroud',
lambda state: state.has("Beat Media Blitz", player))
connect(world, names, 'Maw of the Void', 'Gates of Hell',
lambda state: state.has("Beat Maw of the Void", player))
connect(world, names, 'Gates of Hell', 'Belly of the Beast',
lambda state: state.has("Beat Gates of Hell", player))
connect(world, names, 'Gates of Hell', 'Shatter the Sky',
lambda state: state.has("Beat Gates of Hell", player))
connect(world, 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)))
if SC2Campaign.PROPHECY in enabled_campaigns:
if SC2Campaign.WOL in enabled_campaigns:
connect(world, names, 'The Dig', 'Whispers of Doom',
lambda state: state.has("Beat The Dig", player)),
else:
vanilla_mission_reqs[SC2Campaign.PROPHECY] = vanilla_mission_reqs[SC2Campaign.PROPHECY].copy()
vanilla_mission_reqs[SC2Campaign.PROPHECY][SC2Mission.WHISPERS_OF_DOOM.mission_name] = MissionInfo(
SC2Mission.WHISPERS_OF_DOOM, [], SC2Mission.WHISPERS_OF_DOOM.area)
connect(world, names, 'Menu', 'Whispers of Doom'),
connect(world, names, 'Whispers of Doom', 'A Sinister Turn',
lambda state: state.has("Beat Whispers of Doom", player))
connect(world, names, 'A Sinister Turn', 'Echoes of the Future',
lambda state: state.has("Beat A Sinister Turn", player))
connect(world, names, 'Echoes of the Future', 'In Utter Darkness',
lambda state: state.has("Beat Echoes of the Future", player))
if SC2Campaign.HOTS in enabled_campaigns:
connect(world, names, 'Menu', 'Lab Rat'),
connect(world, names, 'Lab Rat', 'Back in the Saddle',
lambda state: state.has("Beat Lab Rat", player)),
connect(world, names, 'Back in the Saddle', 'Rendezvous',
lambda state: state.has("Beat Back in the Saddle", player)),
connect(world, names, 'Rendezvous', 'Harvest of Screams',
lambda state: state.has("Beat Rendezvous", player)),
connect(world, names, 'Harvest of Screams', 'Shoot the Messenger',
lambda state: state.has("Beat Harvest of Screams", player)),
connect(world, names, 'Shoot the Messenger', 'Enemy Within',
lambda state: state.has("Beat Shoot the Messenger", player)),
connect(world, names, 'Rendezvous', 'Domination',
lambda state: state.has("Beat Rendezvous", player)),
connect(world, names, 'Domination', 'Fire in the Sky',
lambda state: state.has("Beat Domination", player)),
connect(world, names, 'Fire in the Sky', 'Old Soldiers',
lambda state: state.has("Beat Fire in the Sky", player)),
connect(world, names, 'Old Soldiers', 'Waking the Ancient',
lambda state: state.has("Beat Old Soldiers", player)),
connect(world, names, 'Enemy Within', 'Waking the Ancient',
lambda state: state.has("Beat Enemy Within", player)),
connect(world, names, 'Waking the Ancient', 'The Crucible',
lambda state: state.has("Beat Waking the Ancient", player)),
connect(world, names, 'The Crucible', 'Supreme',
lambda state: state.has("Beat The Crucible", player)),
connect(world, names, 'Supreme', 'Infested',
lambda state: state.has("Beat Supreme", player) and
state.has("Beat Old Soldiers", player) and
state.has("Beat Enemy Within", player)),
connect(world, names, 'Infested', 'Hand of Darkness',
lambda state: state.has("Beat Infested", player)),
connect(world, names, 'Hand of Darkness', 'Phantoms of the Void',
lambda state: state.has("Beat Hand of Darkness", player)),
connect(world, names, 'Supreme', 'With Friends Like These',
lambda state: state.has("Beat Supreme", player) and
state.has("Beat Old Soldiers", player) and
state.has("Beat Enemy Within", player)),
connect(world, names, 'With Friends Like These', 'Conviction',
lambda state: state.has("Beat With Friends Like These", player)),
connect(world, names, 'Conviction', 'Planetfall',
lambda state: state.has("Beat Conviction", player) and
state.has("Beat Phantoms of the Void", player)),
connect(world, names, 'Planetfall', 'Death From Above',
lambda state: state.has("Beat Planetfall", player)),
connect(world, names, 'Death From Above', 'The Reckoning',
lambda state: state.has("Beat Death From Above", player)),
if SC2Campaign.PROLOGUE in enabled_campaigns:
connect(world, names, "Menu", "Dark Whispers")
connect(world, names, "Dark Whispers", "Ghosts in the Fog",
lambda state: state.has("Beat Dark Whispers", player))
connect(world, names, "Ghosts in the Fog", "Evil Awoken",
lambda state: state.has("Beat Ghosts in the Fog", player))
if SC2Campaign.LOTV in enabled_campaigns:
connect(world, names, "Menu", "For Aiur!")
connect(world, names, "For Aiur!", "The Growing Shadow",
lambda state: state.has("Beat For Aiur!", player)),
connect(world, names, "The Growing Shadow", "The Spear of Adun",
lambda state: state.has("Beat The Growing Shadow", player)),
connect(world, names, "The Spear of Adun", "Sky Shield",
lambda state: state.has("Beat The Spear of Adun", player)),
connect(world, names, "Sky Shield", "Brothers in Arms",
lambda state: state.has("Beat Sky Shield", player)),
connect(world, names, "Brothers in Arms", "Forbidden Weapon",
lambda state: state.has("Beat Brothers in Arms", player)),
connect(world, names, "The Spear of Adun", "Amon's Reach",
lambda state: state.has("Beat The Spear of Adun", player)),
connect(world, names, "Amon's Reach", "Last Stand",
lambda state: state.has("Beat Amon's Reach", player)),
connect(world, names, "Last Stand", "Forbidden Weapon",
lambda state: state.has("Beat Last Stand", player)),
connect(world, names, "Forbidden Weapon", "Temple of Unification",
lambda state: state.has("Beat Brothers in Arms", player)
and state.has("Beat Last Stand", player)
and state.has("Beat Forbidden Weapon", player)),
connect(world, names, "Temple of Unification", "The Infinite Cycle",
lambda state: state.has("Beat Temple of Unification", player)),
connect(world, names, "The Infinite Cycle", "Harbinger of Oblivion",
lambda state: state.has("Beat The Infinite Cycle", player)),
connect(world, names, "Harbinger of Oblivion", "Unsealing the Past",
lambda state: state.has("Beat Harbinger of Oblivion", player)),
connect(world, names, "Unsealing the Past", "Purification",
lambda state: state.has("Beat Unsealing the Past", player)),
connect(world, names, "Purification", "Templar's Charge",
lambda state: state.has("Beat Purification", player)),
connect(world, names, "Harbinger of Oblivion", "Steps of the Rite",
lambda state: state.has("Beat Harbinger of Oblivion", player)),
connect(world, names, "Steps of the Rite", "Rak'Shir",
lambda state: state.has("Beat Steps of the Rite", player)),
connect(world, names, "Rak'Shir", "Templar's Charge",
lambda state: state.has("Beat Rak'Shir", player)),
connect(world, names, "Templar's Charge", "Templar's Return",
lambda state: state.has("Beat Purification", player)
and state.has("Beat Rak'Shir", player)
and state.has("Beat Templar's Charge", player)),
connect(world, names, "Templar's Return", "The Host",
lambda state: state.has("Beat Templar's Return", player)),
connect(world, names, "The Host", "Salvation",
lambda state: state.has("Beat The Host", player)),
if SC2Campaign.EPILOGUE in enabled_campaigns:
# TODO: Make this aware about excluded campaigns
connect(world, names, "Salvation", "Into the Void",
lambda state: state.has("Beat Salvation", player)
and state.has("Beat The Reckoning", player)
and state.has("Beat All-In", player)),
connect(world, names, "Into the Void", "The Essence of Eternity",
lambda state: state.has("Beat Into the Void", player)),
connect(world, names, "The Essence of Eternity", "Amon's Fall",
lambda state: state.has("Beat The Essence of Eternity", player)),
if SC2Campaign.NCO in enabled_campaigns:
connect(world, names, "Menu", "The Escape")
connect(world, names, "The Escape", "Sudden Strike",
lambda state: state.has("Beat The Escape", player))
connect(world, names, "Sudden Strike", "Enemy Intelligence",
lambda state: state.has("Beat Sudden Strike", player))
connect(world, names, "Enemy Intelligence", "Trouble In Paradise",
lambda state: state.has("Beat Enemy Intelligence", player))
connect(world, names, "Trouble In Paradise", "Night Terrors",
lambda state: state.has("Beat Trouble In Paradise", player))
connect(world, names, "Night Terrors", "Flashpoint",
lambda state: state.has("Beat Night Terrors", player))
connect(world, names, "Flashpoint", "In the Enemy's Shadow",
lambda state: state.has("Beat Flashpoint", player))
connect(world, names, "In the Enemy's Shadow", "Dark Skies",
lambda state: state.has("Beat In the Enemy's Shadow", player))
connect(world, names, "Dark Skies", "End Game",
lambda state: state.has("Beat Dark Skies", player))
goal_location = get_goal_location(final_mission)
assert goal_location, f"Unable to find a goal location for mission {final_mission}"
setup_final_location(goal_location, location_cache)
return (vanilla_mission_reqs, final_mission.id, goal_location)
def create_grid_regions(
world: World,
locations: Tuple[LocationData, ...],
location_cache: List[Location],
) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
locations_per_region = get_locations_per_region(locations)
mission_pools = filter_missions(world)
final_mission = mission_pools[MissionPools.FINAL][0]
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"))
remove_top_left: bool = get_option_value(world, "grid_two_start_positions") == GridTwoStartPositions.option_true
regions = [create_region(world, locations_per_region, location_cache, "Menu")]
names: Dict[str, int] = {}
missions: Dict[Tuple[int, int], SC2Mission] = {}
grid_size_x, grid_size_y, num_corners_to_remove = get_grid_dimensions(num_missions + remove_top_left)
# pick missions in order along concentric diagonals
# each diagonal will have the same difficulty
# this keeps long sides from possibly stealing lower-difficulty missions from future columns
num_diagonals = grid_size_x + grid_size_y - 1
diagonal_difficulty = MissionPools.STARTER
missions_to_add = mission_pools[MissionPools.STARTER]
for diagonal in range(num_diagonals):
if diagonal == num_diagonals - 1:
diagonal_difficulty = MissionPools.FINAL
grid_coords = (grid_size_x-1, grid_size_y-1)
missions[grid_coords] = final_mission
break
if diagonal == 0 and remove_top_left:
continue
diagonal_length = min(diagonal + 1, num_diagonals - diagonal, grid_size_x, grid_size_y)
if len(missions_to_add) < diagonal_length:
raise Exception(f"There are not enough {diagonal_difficulty.name} missions to fill the campaign. Please exclude fewer missions.")
for i in range(diagonal_length):
# (0,0) + (0,1)*diagonal + (1,-1)*i + (1,-1)*max(diagonal - grid_size_y + 1, 0)
grid_coords = (i + max(diagonal - grid_size_y + 1, 0), diagonal - i - max(diagonal - grid_size_y + 1, 0))
if grid_coords == (grid_size_x - 1, 0) and num_corners_to_remove >= 2:
pass
elif grid_coords == (0, grid_size_y - 1) and num_corners_to_remove >= 1:
pass
else:
mission_index = world.random.randint(0, len(missions_to_add) - 1)
missions[grid_coords] = missions_to_add.pop(mission_index)
if diagonal_difficulty < MissionPools.VERY_HARD:
diagonal_difficulty = MissionPools(diagonal_difficulty.value + 1)
missions_to_add.extend(mission_pools[diagonal_difficulty])
# Generating regions and locations from selected missions
for x in range(grid_size_x):
for y in range(grid_size_y):
if missions.get((x, y)):
regions.append(create_region(world, locations_per_region, location_cache, missions[(x, y)].mission_name))
world.multiworld.regions += regions
# This pattern is horrifying, why are we using the dict as an ordered dict???
slot_map: Dict[Tuple[int, int], int] = {}
for index, coords in enumerate(missions):
slot_map[coords] = index + 1
mission_req_table: Dict[str, MissionInfo] = {}
for coords, mission in missions.items():
prepend_vertical = 0
if not mission:
continue
connections: List[MissionConnection] = []
if coords == (0, 0) or (remove_top_left and sum(coords) == 1):
# Connect to the "Menu" starting region
connect(world, names, "Menu", mission.mission_name)
else:
for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
connected_coords = (coords[0] + dx, coords[1] + dy)
if connected_coords in missions:
# connections.append(missions[connected_coords])
connections.append(MissionConnection(slot_map[connected_coords]))
connect(world, names, missions[connected_coords].mission_name, mission.mission_name,
make_grid_connect_rule(missions, connected_coords, world.player),
)
if coords[1] == 1 and not missions.get((coords[0], 0)):
prepend_vertical = 1
mission_req_table[mission.mission_name] = MissionInfo(
mission,
connections,
category=f'_{coords[0] + 1}',
or_requirements=True,
ui_vertical_padding=prepend_vertical,
)
final_mission_id = final_mission.id
# Changing the completion condition for alternate final missions into an event
final_location = get_goal_location(final_mission)
setup_final_location(final_location, location_cache)
return {SC2Campaign.GLOBAL: mission_req_table}, final_mission_id, final_location
def make_grid_connect_rule(
missions: Dict[Tuple[int, int], SC2Mission],
connected_coords: Tuple[int, int],
player: int
) -> Callable[[CollectionState], bool]:
return lambda state: state.has(f"Beat {missions[connected_coords].mission_name}", player)
def create_structured_regions(
world: World,
locations: Tuple[LocationData, ...],
location_cache: List[Location],
mission_order_type: int,
) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
locations_per_region = get_locations_per_region(locations)
mission_order = mission_orders[mission_order_type]()
enabled_campaigns = get_enabled_campaigns(world)
shuffle_campaigns = get_option_value(world, "shuffle_campaigns")
mission_pools: Dict[MissionPools, List[SC2Mission]] = filter_missions(world)
final_mission = mission_pools[MissionPools.FINAL][0]
regions = [create_region(world, locations_per_region, location_cache, "Menu")]
names: Dict[str, int] = {}
mission_slots: List[SC2MissionSlot] = []
mission_pool = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
if mission_order_type in campaign_depending_orders:
# Do slot removal per campaign
for campaign in enabled_campaigns:
campaign_mission_pool = [mission for mission in mission_pool if mission.campaign == campaign]
campaign_mission_pool_size = len(campaign_mission_pool)
removals = len(mission_order[campaign]) - campaign_mission_pool_size
for mission in mission_order[campaign]:
# Removing extra missions if mission pool is too small
if 0 < mission.removal_priority <= removals:
mission_slots.append(SC2MissionSlot(campaign, None))
elif mission.type == MissionPools.FINAL:
if campaign == final_mission.campaign:
# Campaign is elected to be goal
mission_slots.append(SC2MissionSlot(campaign, final_mission))
else:
# Not the goal, find the most difficult mission in the pool and set the difficulty
campaign_difficulty = max(mission.pool for mission in campaign_mission_pool)
mission_slots.append(SC2MissionSlot(campaign, campaign_difficulty))
else:
mission_slots.append(SC2MissionSlot(campaign, mission.type))
else:
order = mission_order[SC2Campaign.GLOBAL]
# Determining if missions must be removed
mission_pool_size = sum(len(mission_pool) for mission_pool in mission_pools.values())
removals = len(order) - mission_pool_size
# Initial fill out of mission list and marking All-In mission
for mission in order:
# Removing extra missions if mission pool is too small
if 0 < mission.removal_priority <= removals:
mission_slots.append(SC2MissionSlot(SC2Campaign.GLOBAL, None))
elif mission.type == MissionPools.FINAL:
mission_slots.append(SC2MissionSlot(SC2Campaign.GLOBAL, final_mission))
else:
mission_slots.append(SC2MissionSlot(SC2Campaign.GLOBAL, mission.type))
no_build_slots = []
easy_slots = []
medium_slots = []
hard_slots = []
very_hard_slots = []
# Search through missions to find slots needed to fill
for i in range(len(mission_slots)):
mission_slot = mission_slots[i]
if mission_slot is None:
continue
if isinstance(mission_slot, SC2MissionSlot):
if mission_slot.slot is None:
continue
if mission_slot.slot == MissionPools.STARTER:
no_build_slots.append(i)
elif mission_slot.slot == MissionPools.EASY:
easy_slots.append(i)
elif mission_slot.slot == MissionPools.MEDIUM:
medium_slots.append(i)
elif mission_slot.slot == MissionPools.HARD:
hard_slots.append(i)
elif mission_slot.slot == MissionPools.VERY_HARD:
very_hard_slots.append(i)
def pick_mission(slot):
if shuffle_campaigns or mission_order_type not in campaign_depending_orders:
# Pick a mission from any campaign
filler = world.random.randint(0, len(missions_to_add) - 1)
mission = missions_to_add.pop(filler)
slot_campaign = mission_slots[slot].campaign
mission_slots[slot] = SC2MissionSlot(slot_campaign, mission)
else:
# Pick a mission from required campaign
slot_campaign = mission_slots[slot].campaign
campaign_mission_candidates = [mission for mission in missions_to_add if mission.campaign == slot_campaign]
mission = world.random.choice(campaign_mission_candidates)
missions_to_add.remove(mission)
mission_slots[slot] = SC2MissionSlot(slot_campaign, mission)
# Add no_build missions to the pool and fill in no_build slots
missions_to_add: List[SC2Mission] = 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:
pick_mission(slot)
# 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:
pick_mission(slot)
# 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:
pick_mission(slot)
# 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:
pick_mission(slot)
# Add very hard missions into pool and fill in very hard slots
missions_to_add = missions_to_add + mission_pools[MissionPools.VERY_HARD]
if len(very_hard_slots) > len(missions_to_add):
raise Exception("There are not enough missions to fill the campaign. Please exclude fewer missions.")
for slot in very_hard_slots:
pick_mission(slot)
# Generating regions and locations from selected missions
for mission_slot in mission_slots:
if isinstance(mission_slot.slot, SC2Mission):
regions.append(create_region(world, locations_per_region, location_cache, mission_slot.slot.mission_name))
world.multiworld.regions += regions
campaigns: List[SC2Campaign]
if mission_order_type in campaign_depending_orders:
campaigns = list(enabled_campaigns)
else:
campaigns = [SC2Campaign.GLOBAL]
mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = {}
campaign_mission_slots: Dict[SC2Campaign, List[SC2MissionSlot]] = \
{
campaign: [mission_slot for mission_slot in mission_slots if campaign == mission_slot.campaign]
for campaign in campaigns
}
slot_map: Dict[SC2Campaign, List[int]] = dict()
for campaign in campaigns:
mission_req_table.update({campaign: dict()})
# Mapping original mission slots to shifted mission slots when missions are removed
slot_map[campaign] = []
slot_offset = 0
for position, mission in enumerate(campaign_mission_slots[campaign]):
slot_map[campaign].append(position - slot_offset + 1)
if mission is None or mission.slot is None:
slot_offset += 1
def build_connection_rule(mission_names: List[str], missions_req: int) -> Callable:
player = world.player
if len(mission_names) > 1:
return lambda state: state.has_all({f"Beat {name}" for name in mission_names}, player) \
and state.has_group("Missions", player, missions_req)
else:
return lambda state: state.has(f"Beat {mission_names[0]}", player) \
and state.has_group("Missions", player, missions_req)
for campaign in campaigns:
# Loop through missions to create requirements table and connect regions
for i, mission in enumerate(campaign_mission_slots[campaign]):
if mission is None or mission.slot is None:
continue
connections: List[MissionConnection] = []
all_connections: List[SC2MissionSlot] = []
connection: MissionConnection
for connection in mission_order[campaign][i].connect_to:
if connection.connect_to == -1:
continue
# If mission normally connects to an excluded campaign, connect to menu instead
if connection.campaign not in campaign_mission_slots:
connection.connect_to = -1
continue
while campaign_mission_slots[connection.campaign][connection.connect_to].slot is None:
connection.connect_to -= 1
all_connections.append(campaign_mission_slots[connection.campaign][connection.connect_to])
for connection in mission_order[campaign][i].connect_to:
if connection.connect_to == -1:
connect(world, names, "Menu", mission.slot.mission_name)
else:
required_mission = campaign_mission_slots[connection.campaign][connection.connect_to]
if ((required_mission is None or required_mission.slot is None)
and not mission_order[campaign][i].completion_critical): # Drop non-critical null slots
continue
while required_mission is None or required_mission.slot is None: # Substituting null slot with prior slot
connection.connect_to -= 1
required_mission = campaign_mission_slots[connection.campaign][connection.connect_to]
required_missions = [required_mission] if mission_order[campaign][i].or_requirements else all_connections
if isinstance(required_mission.slot, SC2Mission):
required_mission_name = required_mission.slot.mission_name
required_missions_names = [mission.slot.mission_name for mission in required_missions]
connect(world, names, required_mission_name, mission.slot.mission_name,
build_connection_rule(required_missions_names, mission_order[campaign][i].number))
connections.append(MissionConnection(slot_map[connection.campaign][connection.connect_to], connection.campaign))
mission_req_table[campaign].update({mission.slot.mission_name: MissionInfo(
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)})
final_mission_id = final_mission.id
# Changing the completion condition for alternate final missions into an event
final_location = get_goal_location(final_mission)
setup_final_location(final_location, location_cache)
return mission_req_table, final_mission_id, final_location
def setup_final_location(final_location, location_cache):
# 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].address = None
break
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
location_cache.append(location)
return location
def create_region(world: World, locations_per_region: Dict[str, List[LocationData]],
location_cache: List[Location], name: str) -> Region:
region = Region(name, world.player, world.multiworld)
if name in locations_per_region:
for location_data in locations_per_region[name]:
location = create_location(world.player, location_data, region, location_cache)
region.locations.append(location)
return region
def connect(world: World, used_names: Dict[str, int], source: str, target: str,
rule: Optional[Callable] = None):
source_region = world.get_region(source)
target_region = world.get_region(target)
if target not in used_names:
used_names[target] = 1
name = target
else:
used_names[target] += 1
name = target + (' ' * used_names[target])
connection = Entrance(world.player, name, source_region)
if rule:
connection.access_rule = rule
source_region.exits.append(connection)
connection.connect(target_region)
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
def get_factors(number: int) -> Tuple[int, int]:
"""
Simple factorization into pairs of numbers (x, y) using a sieve method.
Returns the factorization that is most square, i.e. where x + y is minimized.
Factor order is such that x <= y.
"""
assert number > 0
for divisor in range(math.floor(math.sqrt(number)), 1, -1):
quotient = number // divisor
if quotient * divisor == number:
return divisor, quotient
return 1, number
def get_grid_dimensions(size: int) -> Tuple[int, int, int]:
"""
Get the dimensions of a grid mission order from the number of missions, int the format (x, y, error).
* Error will always be 0, 1, or 2, so the missions can be removed from the corners that aren't the start or end.
* Dimensions are chosen such that x <= y, as buttons in the UI are wider than they are tall.
* Dimensions are chosen to be maximally square. That is, x + y + error is minimized.
* If multiple options of the same rating are possible, the one with the larger error is chosen,
as it will appear more square. Compare 3x11 to 5x7-2 for an example of this.
"""
dimension_candidates: List[Tuple[int, int, int]] = [(*get_factors(size + x), x) for x in (2, 1, 0)]
best_dimension = min(dimension_candidates, key=sum)
return best_dimension

View File

@@ -1,952 +0,0 @@
from typing import Set
from BaseClasses import CollectionState
from .Options import get_option_value, RequiredTactics, kerrigan_unit_available, AllInMap, \
GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence, \
get_enabled_campaigns, MissionOrder
from .Items import get_basic_units, defense_ratings, zerg_defense_ratings, kerrigan_actives, air_defense_ratings, \
kerrigan_levels, get_full_item_list
from .MissionTables import SC2Race, SC2Campaign
from . import ItemNames
from worlds.AutoWorld import World
class SC2Logic:
def lock_any_item(self, state: CollectionState, items: Set[str]) -> bool:
"""
Guarantees that at least one of these items will remain in the world. Doesn't affect placement.
Needed for cases when the dynamic pool filtering could remove all the item prerequisites
:param state:
:param items:
:return:
"""
return self.is_item_placement(state) \
or state.has_any(items, self.player)
def is_item_placement(self, state):
"""
Tells if it's item placement or item pool filter
:param state:
:return: True for item placement, False for pool filter
"""
# has_group with count = 0 is always true for item placement and always false for SC2 item filtering
return state.has_group("Missions", self.player, 0)
# WoL
def terran_common_unit(self, state: CollectionState) -> bool:
return state.has_any(self.basic_terran_units, self.player)
def terran_early_tech(self, state: CollectionState):
"""
Basic combat unit that can be deployed quickly from mission start
:param state
:return:
"""
return (
state.has_any({ItemNames.MARINE, ItemNames.FIREBAT, ItemNames.MARAUDER, ItemNames.REAPER, ItemNames.HELLION}, self.player)
or (self.advanced_tactics and state.has_any({ItemNames.GOLIATH, ItemNames.DIAMONDBACK, ItemNames.VIKING, ItemNames.BANSHEE}, self.player))
)
def terran_air(self, state: CollectionState) -> bool:
"""
Air units or drops on advanced tactics
:param state:
:return:
"""
return (state.has_any({ItemNames.VIKING, ItemNames.WRAITH, ItemNames.BANSHEE, ItemNames.BATTLECRUISER}, self.player) or self.advanced_tactics
and state.has_any({ItemNames.HERCULES, ItemNames.MEDIVAC}, self.player) and self.terran_common_unit(state)
)
def terran_air_anti_air(self, state: CollectionState) -> bool:
"""
Air-to-air
:param state:
:return:
"""
return (
state.has(ItemNames.VIKING, self.player)
or state.has_all({ItemNames.WRAITH, ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player)
or state.has_all({ItemNames.BATTLECRUISER, ItemNames.BATTLECRUISER_ATX_LASER_BATTERY}, self.player)
or self.advanced_tactics and state.has_any({ItemNames.WRAITH, ItemNames.VALKYRIE, ItemNames.BATTLECRUISER}, self.player)
)
def terran_competent_ground_to_air(self, state: CollectionState) -> bool:
"""
Ground-to-air
:param state:
:return:
"""
return (
state.has(ItemNames.GOLIATH, self.player)
or state.has(ItemNames.MARINE, self.player) and self.terran_bio_heal(state)
or self.advanced_tactics and state.has(ItemNames.CYCLONE, self.player)
)
def terran_competent_anti_air(self, state: CollectionState) -> bool:
"""
Good AA unit
:param state:
:return:
"""
return (
self.terran_competent_ground_to_air(state)
or self.terran_air_anti_air(state)
)
def welcome_to_the_jungle_requirement(self, state: CollectionState) -> bool:
"""
Welcome to the Jungle requirements - able to deal with Scouts, Void Rays, Zealots and Stalkers
:param state:
:return:
"""
return (
self.terran_common_unit(state)
and self.terran_competent_ground_to_air(state)
) or (
self.advanced_tactics
and state.has_any({ItemNames.MARINE, ItemNames.VULTURE}, self.player)
and self.terran_air_anti_air(state)
)
def terran_basic_anti_air(self, state: CollectionState) -> bool:
"""
Basic AA to deal with few air units
:param state:
:return:
"""
return (
state.has_any({
ItemNames.MISSILE_TURRET, ItemNames.THOR, ItemNames.WAR_PIGS, ItemNames.SPARTAN_COMPANY,
ItemNames.HELS_ANGELS, ItemNames.BATTLECRUISER, ItemNames.MARINE, ItemNames.WRAITH,
ItemNames.VALKYRIE, ItemNames.CYCLONE, ItemNames.WINGED_NIGHTMARES, ItemNames.BRYNHILDS
}, self.player)
or self.terran_competent_anti_air(state)
or self.advanced_tactics and state.has_any({ItemNames.GHOST, ItemNames.SPECTRE, ItemNames.WIDOW_MINE, ItemNames.LIBERATOR}, self.player)
)
def terran_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_enemy: bool = True) -> int:
"""
Ability to handle defensive missions
:param state:
:param zerg_enemy:
:param air_enemy:
:return:
"""
defense_score = sum((defense_ratings[item] for item in defense_ratings if state.has(item, self.player)))
# Manned Bunker
if state.has_any({ItemNames.MARINE, ItemNames.MARAUDER}, self.player) and state.has(ItemNames.BUNKER, self.player):
defense_score += 3
elif zerg_enemy and state.has(ItemNames.FIREBAT, self.player) and state.has(ItemNames.BUNKER, self.player):
defense_score += 2
# Siege Tank upgrades
if state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_MAELSTROM_ROUNDS}, self.player):
defense_score += 2
if state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_GRADUATING_RANGE}, self.player):
defense_score += 1
# Widow Mine upgrade
if state.has_all({ItemNames.WIDOW_MINE, ItemNames.WIDOW_MINE_CONCEALMENT}, self.player):
defense_score += 1
# Viking with splash
if state.has_all({ItemNames.VIKING, ItemNames.VIKING_SHREDDER_ROUNDS}, self.player):
defense_score += 2
# General enemy-based rules
if zerg_enemy:
defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if state.has(item, self.player)))
if air_enemy:
defense_score += sum((air_defense_ratings[item] for item in air_defense_ratings if state.has(item, self.player)))
if air_enemy and zerg_enemy and state.has(ItemNames.VALKYRIE, self.player):
# Valkyries shred mass Mutas, most common air enemy that's massed in these cases
defense_score += 2
# Advanced Tactics bumps defense rating requirements down by 2
if self.advanced_tactics:
defense_score += 2
return defense_score
def terran_competent_comp(self, state: CollectionState) -> bool:
"""
Ability to deal with most of hard missions
:param state:
:return:
"""
return (
(
(state.has_any({ItemNames.MARINE, ItemNames.MARAUDER}, self.player) and self.terran_bio_heal(state))
or state.has_any({ItemNames.THOR, ItemNames.BANSHEE, ItemNames.SIEGE_TANK}, self.player)
or state.has_all({ItemNames.LIBERATOR, ItemNames.LIBERATOR_RAID_ARTILLERY}, self.player)
)
and self.terran_competent_anti_air(state)
) or (
state.has(ItemNames.BATTLECRUISER, self.player) and self.terran_common_unit(state)
)
def great_train_robbery_train_stopper(self, state: CollectionState) -> bool:
"""
Ability to deal with trains (moving target with a lot of HP)
:param state:
:return:
"""
return (
state.has_any({ItemNames.SIEGE_TANK, ItemNames.DIAMONDBACK, ItemNames.MARAUDER, ItemNames.CYCLONE, ItemNames.BANSHEE}, self.player)
or self.advanced_tactics
and (
state.has_all({ItemNames.REAPER, ItemNames.REAPER_G4_CLUSTERBOMB}, self.player)
or state.has_all({ItemNames.SPECTRE, ItemNames.SPECTRE_PSIONIC_LASH}, self.player)
or state.has_any({ItemNames.VULTURE, ItemNames.LIBERATOR}, self.player)
)
)
def terran_can_rescue(self, state) -> bool:
"""
Rescuing in The Moebius Factor
:param state:
:return:
"""
return state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES, ItemNames.RAVEN, ItemNames.VIKING}, self.player) or self.advanced_tactics
def terran_beats_protoss_deathball(self, state: CollectionState) -> bool:
"""
Ability to deal with Immortals, Colossi with some air support
:param state:
:return:
"""
return (
(
state.has_any({ItemNames.BANSHEE, ItemNames.BATTLECRUISER}, self.player)
or state.has_all({ItemNames.LIBERATOR, ItemNames.LIBERATOR_RAID_ARTILLERY}, self.player)
) and self.terran_competent_anti_air(state)
or self.terran_competent_comp(state) and self.terran_air_anti_air(state)
)
def marine_medic_upgrade(self, state: CollectionState) -> bool:
"""
Infantry upgrade to infantry-only no-build segments
:param state:
:return:
"""
return state.has_any({
ItemNames.MARINE_COMBAT_SHIELD, ItemNames.MARINE_MAGRAIL_MUNITIONS, ItemNames.MEDIC_STABILIZER_MEDPACKS
}, self.player) \
or (state.count(ItemNames.MARINE_PROGRESSIVE_STIMPACK, self.player) >= 2
and state.has_group("Missions", self.player, 1))
def terran_survives_rip_field(self, state: CollectionState) -> bool:
"""
Ability to deal with large areas with environment damage
:param state:
:return:
"""
return (state.has(ItemNames.BATTLECRUISER, self.player)
or self.terran_air(state) and self.terran_competent_anti_air(state) and self.terran_sustainable_mech_heal(state))
def terran_sustainable_mech_heal(self, state: CollectionState) -> bool:
"""
Can heal mech units without spending resources
:param state:
:return:
"""
return state.has(ItemNames.SCIENCE_VESSEL, self.player) \
or state.has_all({ItemNames.MEDIC, ItemNames.MEDIC_ADAPTIVE_MEDPACKS}, self.player) \
or state.count(ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL, self.player) >= 3 \
or (self.advanced_tactics
and (
state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_BIO_MECHANICAL_REPAIR_DRONE}, self.player)
or state.count(ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL, self.player) >= 2)
)
def terran_bio_heal(self, state: CollectionState) -> bool:
"""
Ability to heal bio units
:param state:
:return:
"""
return state.has_any({ItemNames.MEDIC, ItemNames.MEDIVAC}, self.player) \
or self.advanced_tactics and state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_BIO_MECHANICAL_REPAIR_DRONE}, self.player)
def terran_base_trasher(self, state: CollectionState) -> bool:
"""
Can attack heavily defended bases
:param state:
:return:
"""
return state.has(ItemNames.SIEGE_TANK, self.player) \
or state.has_all({ItemNames.BATTLECRUISER, ItemNames.BATTLECRUISER_ATX_LASER_BATTERY}, self.player) \
or state.has_all({ItemNames.LIBERATOR, ItemNames.LIBERATOR_RAID_ARTILLERY}, self.player) \
or (self.advanced_tactics
and ((state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, self.player)
or self.can_nuke(state))
and (
state.has_all({ItemNames.VIKING, ItemNames.VIKING_SHREDDER_ROUNDS}, self.player)
or state.has_all({ItemNames.BANSHEE, ItemNames.BANSHEE_SHOCKWAVE_MISSILE_BATTERY}, self.player))
)
)
def terran_mobile_detector(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.RAVEN, ItemNames.SCIENCE_VESSEL, ItemNames.PROGRESSIVE_ORBITAL_COMMAND}, self.player)
def can_nuke(self, state: CollectionState) -> bool:
"""
Ability to launch nukes
:param state:
:return:
"""
return (self.advanced_tactics
and (state.has_any({ItemNames.GHOST, ItemNames.SPECTRE}, self.player)
or state.has_all({ItemNames.THOR, ItemNames.THOR_BUTTON_WITH_A_SKULL_ON_IT}, self.player)))
def terran_respond_to_colony_infestations(self, state: CollectionState) -> bool:
"""
Can deal quickly with Brood Lords and Mutas in Haven's Fall and being able to progress the mission
:param state:
:return:
"""
return (
self.terran_common_unit(state)
and self.terran_competent_anti_air(state)
and (
self.terran_air_anti_air(state)
or state.has_any({ItemNames.BATTLECRUISER, ItemNames.VALKYRIE}, self.player)
)
and self.terran_defense_rating(state, True) >= 3
)
def engine_of_destruction_requirement(self, state: CollectionState):
return self.marine_medic_upgrade(state) \
and (
self.terran_competent_anti_air(state)
and self.terran_common_unit(state) or state.has(ItemNames.WRAITH, self.player)
)
def all_in_requirement(self, state: CollectionState):
"""
All-in
:param state:
:return:
"""
beats_kerrigan = state.has_any({ItemNames.MARINE, ItemNames.BANSHEE, ItemNames.GHOST}, self.player) or self.advanced_tactics
if get_option_value(self.world, 'all_in_map') == AllInMap.option_ground:
# Ground
defense_rating = self.terran_defense_rating(state, True, False)
if state.has_any({ItemNames.BATTLECRUISER, ItemNames.BANSHEE}, self.player):
defense_rating += 2
return defense_rating >= 13 and beats_kerrigan
else:
# Air
defense_rating = self.terran_defense_rating(state, True, True)
return defense_rating >= 9 and beats_kerrigan \
and state.has_any({ItemNames.VIKING, ItemNames.BATTLECRUISER, ItemNames.VALKYRIE}, self.player) \
and state.has_any({ItemNames.HIVE_MIND_EMULATOR, ItemNames.PSI_DISRUPTER, ItemNames.MISSILE_TURRET}, self.player)
# HotS
def zerg_common_unit(self, state: CollectionState) -> bool:
return state.has_any(self.basic_zerg_units, self.player)
def zerg_competent_anti_air(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.HYDRALISK, ItemNames.MUTALISK, ItemNames.CORRUPTOR, ItemNames.BROOD_QUEEN}, self.player) \
or state.has_all({ItemNames.SWARM_HOST, ItemNames.SWARM_HOST_PRESSURIZED_GLANDS}, self.player) \
or state.has_all({ItemNames.SCOURGE, ItemNames.SCOURGE_RESOURCE_EFFICIENCY}, self.player) \
or (self.advanced_tactics and state.has(ItemNames.INFESTOR, self.player))
def zerg_basic_anti_air(self, state: CollectionState) -> bool:
return self.zerg_competent_anti_air(state) or self.kerrigan_unit_available in kerrigan_unit_available or \
state.has_any({ItemNames.SWARM_QUEEN, ItemNames.SCOURGE}, self.player) or (self.advanced_tactics and state.has(ItemNames.SPORE_CRAWLER, self.player))
def morph_brood_lord(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.MUTALISK, ItemNames.CORRUPTOR}, self.player) \
and state.has(ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, self.player)
def morph_viper(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.MUTALISK, ItemNames.CORRUPTOR}, self.player) \
and state.has(ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT, self.player)
def morph_impaler_or_lurker(self, state: CollectionState) -> bool:
return state.has(ItemNames.HYDRALISK, self.player) and state.has_any({ItemNames.HYDRALISK_IMPALER_ASPECT, ItemNames.HYDRALISK_LURKER_ASPECT}, self.player)
def zerg_competent_comp(self, state: CollectionState) -> bool:
advanced = self.advanced_tactics
core_unit = state.has_any({ItemNames.ROACH, ItemNames.ABERRATION, ItemNames.ZERGLING}, self.player)
support_unit = state.has_any({ItemNames.SWARM_QUEEN, ItemNames.HYDRALISK}, self.player) \
or self.morph_brood_lord(state) \
or advanced and (state.has_any({ItemNames.INFESTOR, ItemNames.DEFILER}, self.player) or self.morph_viper(state))
if core_unit and support_unit:
return True
vespene_unit = state.has_any({ItemNames.ULTRALISK, ItemNames.ABERRATION}, self.player) \
or advanced and self.morph_viper(state)
return vespene_unit and state.has_any({ItemNames.ZERGLING, ItemNames.SWARM_QUEEN}, self.player)
def spread_creep(self, state: CollectionState) -> bool:
return self.advanced_tactics or state.has(ItemNames.SWARM_QUEEN, self.player)
def zerg_competent_defense(self, state: CollectionState) -> bool:
return (
self.zerg_common_unit(state)
and (
(
state.has(ItemNames.SWARM_HOST, self.player)
or self.morph_brood_lord(state)
or self.morph_impaler_or_lurker(state)
) or (
self.advanced_tactics
and (self.morph_viper(state)
or state.has(ItemNames.SPINE_CRAWLER, self.player))
)
)
)
def basic_kerrigan(self, state: CollectionState) -> bool:
# One active ability that can be used to defeat enemies directly on Standard
if not self.advanced_tactics and \
not state.has_any({ItemNames.KERRIGAN_KINETIC_BLAST, ItemNames.KERRIGAN_LEAPING_STRIKE,
ItemNames.KERRIGAN_CRUSHING_GRIP, ItemNames.KERRIGAN_PSIONIC_SHIFT,
ItemNames.KERRIGAN_SPAWN_BANELINGS}, self.player):
return False
# Two non-ultimate abilities
count = 0
for item in (ItemNames.KERRIGAN_KINETIC_BLAST, ItemNames.KERRIGAN_LEAPING_STRIKE, ItemNames.KERRIGAN_HEROIC_FORTITUDE,
ItemNames.KERRIGAN_CHAIN_REACTION, ItemNames.KERRIGAN_CRUSHING_GRIP, ItemNames.KERRIGAN_PSIONIC_SHIFT,
ItemNames.KERRIGAN_SPAWN_BANELINGS, ItemNames.KERRIGAN_INFEST_BROODLINGS, ItemNames.KERRIGAN_FURY):
if state.has(item, self.player):
count += 1
if count >= 2:
return True
return False
def two_kerrigan_actives(self, state: CollectionState) -> bool:
count = 0
for i in range(7):
if state.has_any(kerrigan_actives[i], self.player):
count += 1
return count >= 2
def zerg_pass_vents(self, state: CollectionState) -> bool:
return self.story_tech_granted \
or state.has_any({ItemNames.ZERGLING, ItemNames.HYDRALISK, ItemNames.ROACH}, self.player) \
or (self.advanced_tactics and state.has(ItemNames.INFESTOR, self.player))
def supreme_requirement(self, state: CollectionState) -> bool:
return self.story_tech_granted \
or not self.kerrigan_unit_available \
or (
state.has_all({ItemNames.KERRIGAN_LEAPING_STRIKE, ItemNames.KERRIGAN_MEND}, self.player)
and self.kerrigan_levels(state, 35)
)
def kerrigan_levels(self, state: CollectionState, target: int) -> bool:
if self.story_levels_granted or not self.kerrigan_unit_available:
return True # Levels are granted
if self.kerrigan_levels_per_mission_completed > 0 \
and self.kerrigan_levels_per_mission_completed_cap > 0 \
and not self.is_item_placement(state):
# Levels can be granted from mission completion.
# Item pool filtering isn't aware of missions beaten. Assume that missions beaten will fulfill this rule.
return True
# Levels from missions beaten
levels = self.kerrigan_levels_per_mission_completed * state.count_group("Missions", self.player)
if self.kerrigan_levels_per_mission_completed_cap != -1:
levels = min(levels, self.kerrigan_levels_per_mission_completed_cap)
# Levels from items
for kerrigan_level_item in kerrigan_levels:
level_amount = get_full_item_list()[kerrigan_level_item].number
item_count = state.count(kerrigan_level_item, self.player)
levels += item_count * level_amount
# Total level cap
if self.kerrigan_total_level_cap != -1:
levels = min(levels, self.kerrigan_total_level_cap)
return levels >= target
def the_reckoning_requirement(self, state: CollectionState) -> bool:
if self.take_over_ai_allies:
return self.terran_competent_comp(state) \
and self.zerg_competent_comp(state) \
and (self.zerg_competent_anti_air(state)
or self.terran_competent_anti_air(state))
else:
return self.zerg_competent_comp(state) \
and self.zerg_competent_anti_air(state)
# LotV
def protoss_common_unit(self, state: CollectionState) -> bool:
return state.has_any(self.basic_protoss_units, self.player)
def protoss_basic_anti_air(self, state: CollectionState) -> bool:
return self.protoss_competent_anti_air(state) \
or state.has_any({ItemNames.PHOENIX, ItemNames.MIRAGE, ItemNames.CORSAIR, ItemNames.CARRIER, ItemNames.SCOUT,
ItemNames.DARK_ARCHON, ItemNames.WRATHWALKER, ItemNames.MOTHERSHIP}, self.player) \
or state.has_all({ItemNames.WARP_PRISM, ItemNames.WARP_PRISM_PHASE_BLASTER}, self.player) \
or self.advanced_tactics and state.has_any(
{ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ASCENDANT, ItemNames.DARK_TEMPLAR,
ItemNames.SENTRY, ItemNames.ENERGIZER}, self.player)
def protoss_anti_armor_anti_air(self, state: CollectionState) -> bool:
return self.protoss_competent_anti_air(state) \
or state.has_any({ItemNames.SCOUT, ItemNames.WRATHWALKER}, self.player) \
or (state.has_any({ItemNames.IMMORTAL, ItemNames.ANNIHILATOR}, self.player)
and state.has(ItemNames.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS, self.player))
def protoss_anti_light_anti_air(self, state: CollectionState) -> bool:
return self.protoss_competent_anti_air(state) \
or state.has_any({ItemNames.PHOENIX, ItemNames.MIRAGE, ItemNames.CORSAIR, ItemNames.CARRIER}, self.player)
def protoss_competent_anti_air(self, state: CollectionState) -> bool:
return state.has_any(
{ItemNames.STALKER, ItemNames.SLAYER, ItemNames.INSTIGATOR, ItemNames.DRAGOON, ItemNames.ADEPT,
ItemNames.VOID_RAY, ItemNames.DESTROYER, ItemNames.TEMPEST}, self.player) \
or (state.has_any({ItemNames.PHOENIX, ItemNames.MIRAGE, ItemNames.CORSAIR, ItemNames.CARRIER}, self.player)
and state.has_any({ItemNames.SCOUT, ItemNames.WRATHWALKER}, self.player)) \
or (self.advanced_tactics
and state.has_any({ItemNames.IMMORTAL, ItemNames.ANNIHILATOR}, self.player)
and state.has(ItemNames.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS, self.player))
def protoss_has_blink(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER}, self.player) \
or (
state.has(ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK, self.player)
and state.has_any({ItemNames.DARK_TEMPLAR, ItemNames.BLOOD_HUNTER, ItemNames.AVENGER}, self.player)
)
def protoss_can_attack_behind_chasm(self, state: CollectionState) -> bool:
return state.has_any(
{ItemNames.SCOUT, ItemNames.TEMPEST,
ItemNames.CARRIER, ItemNames.VOID_RAY, ItemNames.DESTROYER, ItemNames.MOTHERSHIP}, self.player) \
or self.protoss_has_blink(state) \
or (state.has(ItemNames.WARP_PRISM, self.player)
and (self.protoss_common_unit(state) or state.has(ItemNames.WARP_PRISM_PHASE_BLASTER, self.player))) \
or (self.advanced_tactics
and state.has_any({ItemNames.ORACLE, ItemNames.ARBITER}, self.player))
def protoss_fleet(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.CARRIER, ItemNames.TEMPEST, ItemNames.VOID_RAY, ItemNames.DESTROYER}, self.player)
def templars_return_requirement(self, state: CollectionState) -> bool:
return self.story_tech_granted \
or (
state.has_any({ItemNames.IMMORTAL, ItemNames.ANNIHILATOR}, self.player)
and state.has_any({ItemNames.COLOSSUS, ItemNames.VANGUARD, ItemNames.REAVER, ItemNames.DARK_TEMPLAR}, self.player)
and state.has_any({ItemNames.SENTRY, ItemNames.HIGH_TEMPLAR}, self.player)
)
def brothers_in_arms_requirement(self, state: CollectionState) -> bool:
return (
self.protoss_common_unit(state)
and self.protoss_anti_armor_anti_air(state)
and self.protoss_hybrid_counter(state)
) or (
self.take_over_ai_allies
and (
self.terran_common_unit(state)
or self.protoss_common_unit(state)
)
and (
self.terran_competent_anti_air(state)
or self.protoss_anti_armor_anti_air(state)
)
and (
self.protoss_hybrid_counter(state)
or state.has_any({ItemNames.BATTLECRUISER, ItemNames.LIBERATOR, ItemNames.SIEGE_TANK}, self.player)
or state.has_all({ItemNames.SPECTRE, ItemNames.SPECTRE_PSIONIC_LASH}, self.player)
or (state.has(ItemNames.IMMORTAL, self.player)
and state.has_any({ItemNames.MARINE, ItemNames.MARAUDER}, self.player)
and self.terran_bio_heal(state))
)
)
def protoss_hybrid_counter(self, state: CollectionState) -> bool:
"""
Ground Hybrids
"""
return state.has_any(
{ItemNames.ANNIHILATOR, ItemNames.ASCENDANT, ItemNames.TEMPEST, ItemNames.CARRIER, ItemNames.VOID_RAY,
ItemNames.WRATHWALKER, ItemNames.VANGUARD}, self.player) \
or (state.has(ItemNames.IMMORTAL, self.player) or self.advanced_tactics) and state.has_any(
{ItemNames.STALKER, ItemNames.DRAGOON, ItemNames.ADEPT, ItemNames.INSTIGATOR, ItemNames.SLAYER}, self.player)
def the_infinite_cycle_requirement(self, state: CollectionState) -> bool:
return self.story_tech_granted \
or not self.kerrigan_unit_available \
or (
self.two_kerrigan_actives(state)
and self.basic_kerrigan(state)
and self.kerrigan_levels(state, 70)
)
def protoss_basic_splash(self, state: CollectionState) -> bool:
return state.has_any(
{ItemNames.ZEALOT, ItemNames.COLOSSUS, ItemNames.VANGUARD, ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER,
ItemNames.DARK_TEMPLAR, ItemNames.REAVER, ItemNames.ASCENDANT}, self.player)
def protoss_static_defense(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH}, self.player)
def last_stand_requirement(self, state: CollectionState) -> bool:
return self.protoss_common_unit(state) \
and self.protoss_competent_anti_air(state) \
and self.protoss_static_defense(state) \
and (
self.advanced_tactics
or self.protoss_basic_splash(state)
)
def harbinger_of_oblivion_requirement(self, state: CollectionState) -> bool:
return self.protoss_anti_armor_anti_air(state) and (
self.take_over_ai_allies
or (
self.protoss_common_unit(state)
and self.protoss_hybrid_counter(state)
)
)
def protoss_competent_comp(self, state: CollectionState) -> bool:
return self.protoss_common_unit(state) \
and self.protoss_competent_anti_air(state) \
and self.protoss_hybrid_counter(state) \
and self.protoss_basic_splash(state)
def protoss_stalker_upgrade(self, state: CollectionState) -> bool:
return (
state.has_any(
{
ItemNames.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES,
ItemNames.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION
}, self.player)
and self.lock_any_item(state, {ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER})
)
def steps_of_the_rite_requirement(self, state: CollectionState) -> bool:
return self.protoss_competent_comp(state) \
or (
self.protoss_common_unit(state)
and self.protoss_competent_anti_air(state)
and self.protoss_static_defense(state)
)
def protoss_heal(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.CARRIER, ItemNames.SENTRY, ItemNames.SHIELD_BATTERY, ItemNames.RECONSTRUCTION_BEAM}, self.player)
def templars_charge_requirement(self, state: CollectionState) -> bool:
return self.protoss_heal(state) \
and self.protoss_anti_armor_anti_air(state) \
and (
self.protoss_fleet(state)
or (self.advanced_tactics
and self.protoss_competent_comp(state)
)
)
def the_host_requirement(self, state: CollectionState) -> bool:
return (self.protoss_fleet(state)
and self.protoss_static_defense(state)
) or (
self.protoss_competent_comp(state)
and state.has(ItemNames.SOA_TIME_STOP, self.player)
)
def salvation_requirement(self, state: CollectionState) -> bool:
return [
self.protoss_competent_comp(state),
self.protoss_fleet(state),
self.protoss_static_defense(state)
].count(True) >= 2
def into_the_void_requirement(self, state: CollectionState) -> bool:
return self.protoss_competent_comp(state) \
or (
self.take_over_ai_allies
and (
state.has(ItemNames.BATTLECRUISER, self.player)
or (
state.has(ItemNames.ULTRALISK, self.player)
and self.protoss_competent_anti_air(state)
)
)
)
def essence_of_eternity_requirement(self, state: CollectionState) -> bool:
defense_score = self.terran_defense_rating(state, False, True)
if self.take_over_ai_allies and self.protoss_static_defense(state):
defense_score += 2
return defense_score >= 10 \
and (
self.terran_competent_anti_air(state)
or self.take_over_ai_allies
and self.protoss_competent_anti_air(state)
) \
and (
state.has(ItemNames.BATTLECRUISER, self.player)
or (state.has(ItemNames.BANSHEE, self.player) and state.has_any({ItemNames.VIKING, ItemNames.VALKYRIE},
self.player))
or self.take_over_ai_allies and self.protoss_fleet(state)
) \
and state.has_any({ItemNames.SIEGE_TANK, ItemNames.LIBERATOR}, self.player)
def amons_fall_requirement(self, state: CollectionState) -> bool:
if self.take_over_ai_allies:
return (
(
state.has_any({ItemNames.BATTLECRUISER, ItemNames.CARRIER}, self.player)
)
or (state.has(ItemNames.ULTRALISK, self.player)
and self.protoss_competent_anti_air(state)
and (
state.has_any({ItemNames.LIBERATOR, ItemNames.BANSHEE, ItemNames.VALKYRIE, ItemNames.VIKING}, self.player)
or state.has_all({ItemNames.WRAITH, ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player)
or self.protoss_fleet(state)
)
and (self.terran_sustainable_mech_heal(state)
or (self.spear_of_adun_autonomously_cast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_everywhere
and state.has(ItemNames.RECONSTRUCTION_BEAM, self.player))
)
)
) \
and self.terran_competent_anti_air(state) \
and self.protoss_competent_comp(state) \
and self.zerg_competent_comp(state)
else:
return state.has(ItemNames.MUTALISK, self.player) and self.zerg_competent_comp(state)
def nova_any_weapon(self, state: CollectionState) -> bool:
return state.has_any(
{ItemNames.NOVA_C20A_CANISTER_RIFLE, ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PLASMA_RIFLE,
ItemNames.NOVA_MONOMOLECULAR_BLADE, ItemNames.NOVA_BLAZEFIRE_GUNBLADE}, self.player)
def nova_ranged_weapon(self, state: CollectionState) -> bool:
return state.has_any(
{ItemNames.NOVA_C20A_CANISTER_RIFLE, ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PLASMA_RIFLE},
self.player)
def nova_splash(self, state: CollectionState) -> bool:
return state.has_any({
ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_BLAZEFIRE_GUNBLADE, ItemNames.NOVA_PULSE_GRENADES
}, self.player) \
or self.advanced_tactics and state.has_any(
{ItemNames.NOVA_PLASMA_RIFLE, ItemNames.NOVA_MONOMOLECULAR_BLADE}, self.player)
def nova_dash(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.NOVA_MONOMOLECULAR_BLADE, ItemNames.NOVA_BLINK}, self.player)
def nova_full_stealth(self, state: CollectionState) -> bool:
return state.count(ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player) >= 2
def nova_heal(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.NOVA_ARMORED_SUIT_MODULE, ItemNames.NOVA_STIM_INFUSION}, self.player)
def nova_escape_assist(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.NOVA_BLINK, ItemNames.NOVA_HOLO_DECOY, ItemNames.NOVA_IONIC_FORCE_FIELD}, self.player)
def the_escape_stuff_granted(self) -> bool:
"""
The NCO first mission requires having too much stuff first before actually able to do anything
:return:
"""
return self.story_tech_granted \
or (self.mission_order == MissionOrder.option_vanilla and self.enabled_campaigns == {SC2Campaign.NCO})
def the_escape_first_stage_requirement(self, state: CollectionState) -> bool:
return self.the_escape_stuff_granted() \
or (self.nova_ranged_weapon(state) and (self.nova_full_stealth(state) or self.nova_heal(state)))
def the_escape_requirement(self, state: CollectionState) -> bool:
return self.the_escape_first_stage_requirement(state) \
and (self.the_escape_stuff_granted() or self.nova_splash(state))
def terran_cliffjumper(self, state: CollectionState) -> bool:
return state.has(ItemNames.REAPER, self.player) \
or state.has_all({ItemNames.GOLIATH, ItemNames.GOLIATH_JUMP_JETS}, self.player) \
or state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_JUMP_JETS}, self.player)
def terran_able_to_snipe_defiler(self, state: CollectionState) -> bool:
return state.has_all({ItemNames.NOVA_JUMP_SUIT_MODULE, ItemNames.NOVA_C20A_CANISTER_RIFLE}, self.player) \
or state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_MAELSTROM_ROUNDS, ItemNames.SIEGE_TANK_JUMP_JETS}, self.player)
def sudden_strike_requirement(self, state: CollectionState) -> bool:
return self.sudden_strike_can_reach_objectives(state) \
and self.terran_able_to_snipe_defiler(state) \
and state.has_any({ItemNames.SIEGE_TANK, ItemNames.VULTURE}, self.player) \
and self.nova_splash(state) \
and (self.terran_defense_rating(state, True, False) >= 2
or state.has(ItemNames.NOVA_JUMP_SUIT_MODULE, self.player))
def sudden_strike_can_reach_objectives(self, state: CollectionState) -> bool:
return self.terran_cliffjumper(state) \
or state.has_any({ItemNames.BANSHEE, ItemNames.VIKING}, self.player) \
or (
self.advanced_tactics
and state.has(ItemNames.MEDIVAC, self.player)
and state.has_any({ItemNames.MARINE, ItemNames.MARAUDER, ItemNames.VULTURE, ItemNames.HELLION,
ItemNames.GOLIATH}, self.player)
)
def enemy_intelligence_garrisonable_unit(self, state: CollectionState) -> bool:
"""
Has unit usable as a Garrison in Enemy Intelligence
:param state:
:return:
"""
return state.has_any(
{ItemNames.MARINE, ItemNames.REAPER, ItemNames.MARAUDER, ItemNames.GHOST, ItemNames.SPECTRE,
ItemNames.HELLION, ItemNames.GOLIATH, ItemNames.WARHOUND, ItemNames.DIAMONDBACK, ItemNames.VIKING},
self.player)
def enemy_intelligence_cliff_garrison(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.REAPER, ItemNames.VIKING, ItemNames.MEDIVAC, ItemNames.HERCULES}, self.player) \
or state.has_all({ItemNames.GOLIATH, ItemNames.GOLIATH_JUMP_JETS}, self.player) \
or self.advanced_tactics and state.has_any({ItemNames.HELS_ANGELS, ItemNames.BRYNHILDS}, self.player)
def enemy_intelligence_first_stage_requirement(self, state: CollectionState) -> bool:
return self.enemy_intelligence_garrisonable_unit(state) \
and (self.terran_competent_comp(state)
or (
self.terran_common_unit(state)
and self.terran_competent_anti_air(state)
and state.has(ItemNames.NOVA_NUKE, self.player)
)
) \
and self.terran_defense_rating(state, True, True) >= 5
def enemy_intelligence_second_stage_requirement(self, state: CollectionState) -> bool:
return self.enemy_intelligence_first_stage_requirement(state) \
and self.enemy_intelligence_cliff_garrison(state) \
and (
self.story_tech_granted
or (
self.nova_any_weapon(state)
and (
self.nova_full_stealth(state)
or (self.nova_heal(state)
and self.nova_splash(state)
and self.nova_ranged_weapon(state))
)
)
)
def enemy_intelligence_third_stage_requirement(self, state: CollectionState) -> bool:
return self.enemy_intelligence_second_stage_requirement(state) \
and (
self.story_tech_granted
or (
state.has(ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player)
and self.nova_dash(state)
)
)
def trouble_in_paradise_requirement(self, state: CollectionState) -> bool:
return self.nova_any_weapon(state) \
and self.nova_splash(state) \
and self.terran_beats_protoss_deathball(state) \
and self.terran_defense_rating(state, True, True) >= 7
def night_terrors_requirement(self, state: CollectionState) -> bool:
return self.terran_common_unit(state) \
and self.terran_competent_anti_air(state) \
and (
# These can handle the waves of infested, even volatile ones
state.has(ItemNames.SIEGE_TANK, self.player)
or state.has_all({ItemNames.VIKING, ItemNames.VIKING_SHREDDER_ROUNDS}, self.player)
or (
(
# Regular infesteds
state.has(ItemNames.FIREBAT, self.player)
or state.has_all({ItemNames.HELLION, ItemNames.HELLION_HELLBAT_ASPECT}, self.player)
or (
self.advanced_tactics
and state.has_any({ItemNames.PERDITION_TURRET, ItemNames.PLANETARY_FORTRESS}, self.player)
)
)
and self.terran_bio_heal(state)
and (
# Volatile infesteds
state.has(ItemNames.LIBERATOR, self.player)
or (
self.advanced_tactics
and state.has_any({ItemNames.HERC, ItemNames.VULTURE}, self.player)
)
)
)
)
def flashpoint_far_requirement(self, state: CollectionState) -> bool:
return self.terran_competent_comp(state) \
and self.terran_mobile_detector(state) \
and self.terran_defense_rating(state, True, False) >= 6
def enemy_shadow_tripwires_tool(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.NOVA_FLASHBANG_GRENADES, ItemNames.NOVA_BLINK, ItemNames.NOVA_DOMINATION},
self.player)
def enemy_shadow_door_unlocks_tool(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.NOVA_DOMINATION, ItemNames.NOVA_BLINK, ItemNames.NOVA_JUMP_SUIT_MODULE},
self.player)
def enemy_shadow_domination(self, state: CollectionState) -> bool:
return self.story_tech_granted \
or (self.nova_ranged_weapon(state)
and (self.nova_full_stealth(state)
or state.has(ItemNames.NOVA_JUMP_SUIT_MODULE, self.player)
or (self.nova_heal(state) and self.nova_splash(state))
)
)
def enemy_shadow_first_stage(self, state: CollectionState) -> bool:
return self.enemy_shadow_domination(state) \
and (self.story_tech_granted
or ((self.nova_full_stealth(state) and self.enemy_shadow_tripwires_tool(state))
or (self.nova_heal(state) and self.nova_splash(state))
)
)
def enemy_shadow_second_stage(self, state: CollectionState) -> bool:
return self.enemy_shadow_first_stage(state) \
and (self.story_tech_granted
or self.nova_splash(state)
or self.nova_heal(state)
or self.nova_escape_assist(state)
)
def enemy_shadow_door_controls(self, state: CollectionState) -> bool:
return self.enemy_shadow_second_stage(state) \
and (self.story_tech_granted or self.enemy_shadow_door_unlocks_tool(state))
def enemy_shadow_victory(self, state: CollectionState) -> bool:
return self.enemy_shadow_door_controls(state) \
and (self.story_tech_granted or self.nova_heal(state))
def dark_skies_requirement(self, state: CollectionState) -> bool:
return self.terran_common_unit(state) \
and self.terran_beats_protoss_deathball(state) \
and self.terran_defense_rating(state, False, True) >= 8
def end_game_requirement(self, state: CollectionState) -> bool:
return self.terran_competent_comp(state) \
and self.terran_mobile_detector(state) \
and (
state.has_any({ItemNames.BATTLECRUISER, ItemNames.LIBERATOR, ItemNames.BANSHEE}, self.player)
or state.has_all({ItemNames.WRAITH, ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player)
) \
and (state.has_any({ItemNames.BATTLECRUISER, ItemNames.VIKING, ItemNames.LIBERATOR}, self.player)
or (self.advanced_tactics
and state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, self.player)
)
)
def __init__(self, world: World):
self.world: World = world
self.player = None if world is None else world.player
self.logic_level = get_option_value(world, 'required_tactics')
self.advanced_tactics = self.logic_level != RequiredTactics.option_standard
self.take_over_ai_allies = get_option_value(world, "take_over_ai_allies") == TakeOverAIAllies.option_true
self.kerrigan_unit_available = get_option_value(world, 'kerrigan_presence') in kerrigan_unit_available \
and SC2Campaign.HOTS in get_enabled_campaigns(world)
self.kerrigan_levels_per_mission_completed = get_option_value(world, "kerrigan_levels_per_mission_completed")
self.kerrigan_levels_per_mission_completed_cap = get_option_value(world, "kerrigan_levels_per_mission_completed_cap")
self.kerrigan_total_level_cap = get_option_value(world, "kerrigan_total_level_cap")
self.story_tech_granted = get_option_value(world, "grant_story_tech") == GrantStoryTech.option_true
self.story_levels_granted = get_option_value(world, "grant_story_levels") != GrantStoryLevels.option_disabled
self.basic_terran_units = get_basic_units(world, SC2Race.TERRAN)
self.basic_zerg_units = get_basic_units(world, SC2Race.ZERG)
self.basic_protoss_units = get_basic_units(world, SC2Race.PROTOSS)
self.spear_of_adun_autonomously_cast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence")
self.enabled_campaigns = get_enabled_campaigns(world)
self.mission_order = get_option_value(world, "mission_order")

View File

@@ -1,28 +0,0 @@
<CampaignScroll>
scroll_type: ["content", "bars"]
bar_width: dp(12)
effect_cls: "ScrollEffect"
<MultiCampaignLayout>
cols: 1
size_hint_y: None
height: self.minimum_height + 15
padding: [5,0,dp(12),0]
<CampaignLayout>:
cols: 1
<MissionLayout>:
rows: 1
<MissionCategory>:
cols: 1
spacing: [0,5]
<MissionButton>:
text_size: self.size
markup: True
halign: 'center'
valign: 'middle'
padding: [5,0,5,0]
outline_width: 1

File diff suppressed because it is too large Load Diff

2352
worlds/sc2/client.py Normal file

File diff suppressed because it is too large Load Diff

655
worlds/sc2/client_gui.py Normal file
View File

@@ -0,0 +1,655 @@
from typing import *
import asyncio
import logging
from BaseClasses import ItemClassification
from NetUtils import JSONMessagePart
from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser, LogtoUI
from kivy.app import App
from kivy.clock import Clock
from kivy.core.clipboard import Clipboard
from kivy.uix.gridlayout import GridLayout
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.tooltip import MDTooltip
from kivy.uix.scrollview import ScrollView
from kivy.properties import StringProperty, BooleanProperty, NumericProperty
from .client import SC2Context, calc_unfinished_nodes, is_mission_available, compute_received_items, STARCRAFT2
from .item.item_descriptions import item_descriptions
from .item.item_annotations import ITEM_NAME_ANNOTATIONS
from .mission_order.entry_rules import RuleData, SubRuleRuleData, ItemRuleData
from .mission_tables import lookup_id_to_mission, campaign_race_exceptions, \
SC2Mission, SC2Race
from .locations import LocationType, lookup_location_id_to_type, lookup_location_id_to_flags
from .options import LocationInclusion, MissionOrderScouting
from . import SC2World
class HoverableButton(HoverBehavior, Button):
pass
class MissionButton(HoverableButton, MDTooltip):
tooltip_text = StringProperty("Test")
mission_id = NumericProperty(-1)
is_exit = BooleanProperty(False)
is_goal = BooleanProperty(False)
showing_tooltip = BooleanProperty(False)
def __init__(self, *args, **kwargs):
super(HoverableButton, self).__init__(**kwargs)
self._tooltip = ServerToolTip(text=self.text, markup=True)
self._tooltip.padding = [5, 2, 5, 2]
def on_enter(self):
self._tooltip.text = self.tooltip_text
if self.tooltip_text != "":
self.display_tooltip()
def on_leave(self):
self.remove_tooltip()
def display_tooltip(self, *args):
self.showing_tooltip = True
return super().display_tooltip(*args)
def remove_tooltip(self, *args):
self.showing_tooltip = False
return super().remove_tooltip(*args)
@property
def ctx(self) -> SC2Context:
return App.get_running_app().ctx
class CampaignScroll(ScrollView):
border_on = BooleanProperty(False)
class MultiCampaignLayout(GridLayout):
pass
class DownloadDataWarningMessage(Label):
pass
class CampaignLayout(GridLayout):
pass
class RegionLayout(GridLayout):
pass
class ColumnLayout(GridLayout):
pass
class MissionLayout(GridLayout):
pass
class MissionCategory(GridLayout):
pass
class SC2JSONtoKivyParser(KivyJSONtoTextParser):
def _handle_item_name(self, node: JSONMessagePart):
item_name = node["text"]
if self.ctx.slot_info[node["player"]].game != STARCRAFT2 or item_name not in item_descriptions:
return super()._handle_item_name(node)
flags = node.get("flags", 0)
item_types = []
if flags & ItemClassification.progression:
item_types.append("progression")
if flags & ItemClassification.useful:
item_types.append("useful")
if flags & ItemClassification.trap:
item_types.append("trap")
if not item_types:
item_types.append("normal")
# TODO: Some descriptions are too long and get cut off. Is there a general solution or does someone need to manually check every description?
desc = item_descriptions[item_name].replace(". \n", ".<br>").replace(". ", ".<br>").replace("\n", "<br>")
annotation = ITEM_NAME_ANNOTATIONS.get(item_name)
if annotation is not None:
desc = f"{annotation}<br>{desc}"
ref = "Item Class: " + ", ".join(item_types) + "<br><br>" + desc
node.setdefault("refs", []).append(ref)
return super(KivyJSONtoTextParser, self)._handle_item_name(node)
def _handle_text(self, node: JSONMessagePart):
if node.get("keep_markup", False):
for ref in node.get("refs", []):
node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]"
self.ref_count += 1
return super(KivyJSONtoTextParser, self)._handle_text(node)
else:
return super()._handle_text(node)
class SC2Manager(GameManager):
base_title = "Archipelago Starcraft 2 Client"
campaign_panel: Optional[MultiCampaignLayout] = None
campaign_scroll_panel: Optional[CampaignScroll] = None
last_checked_locations: Set[int] = set()
last_items_received: List[int] = []
last_shown_tooltip: int = -1
last_data_out_of_date = False
mission_buttons: List[MissionButton] = []
launching: Union[bool, int] = False # if int -> mission ID
refresh_from_launching = True
first_check = True
first_mission = ""
button_colors: Dict[SC2Race, Tuple[float, float, float]] = {}
ctx: SC2Context
def __init__(self, ctx: SC2Context) -> None:
super().__init__(ctx)
self.json_to_kivy_parser = SC2JSONtoKivyParser(ctx)
self.minimized = False
def on_start(self) -> None:
from . import gui_config
warnings, window_width, window_height = gui_config.get_window_defaults()
from kivy.core.window import Window
original_size_x, original_size_y = Window.size
Window.size = window_width, window_height
Window.left -= max((window_width - original_size_x) // 2, 0)
Window.top -= max((window_height - original_size_y) // 2, 0)
# Add the logging handler manually here instead of using `logging_pairs` to avoid adding 2 unnecessary tabs
logging.getLogger("Starcraft2").addHandler(LogtoUI(self.log_panels["All"].on_log))
for startup_warning in warnings:
logging.getLogger("Starcraft2").warning(f"Startup WARNING: {startup_warning}")
for race in (SC2Race.TERRAN, SC2Race.PROTOSS, SC2Race.ZERG):
errors, color = gui_config.get_button_color(race.name)
self.button_colors[race] = color
for error in errors:
logging.getLogger("Starcraft2").warning(f"{race.name.title()} button color setting: {error}")
def clear_tooltip(self) -> None:
for button in self.mission_buttons:
button.remove_tooltip()
def shown_tooltip(self) -> int:
for button in self.mission_buttons:
if button.showing_tooltip:
return button.mission_id
return -1
def build(self):
container = super().build()
panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll())
self.campaign_scroll_panel = panel.content
self.campaign_panel = MultiCampaignLayout()
panel.content.add_widget(self.campaign_panel)
Clock.schedule_interval(self.build_mission_table, 0.5)
return container
def build_mission_table(self, dt) -> None:
if self.launching:
assert self.campaign_panel is not None
self.refresh_from_launching = False
self.campaign_panel.clear_widgets()
self.campaign_panel.add_widget(Label(
text="Launching Mission: " + lookup_id_to_mission[self.launching].mission_name
))
if self.ctx.ui:
self.ctx.ui.clear_tooltip()
return
sorted_items_received = sorted([item.item for item in self.ctx.items_received])
shown_tooltip = self.shown_tooltip()
hovering_tooltip = (
self.last_shown_tooltip != -1
and self.last_shown_tooltip == shown_tooltip
)
data_changed = (
self.last_checked_locations != self.ctx.checked_locations
or self.last_items_received != sorted_items_received
)
needs_redraw = (
data_changed
and not hovering_tooltip
or not self.refresh_from_launching
or self.last_data_out_of_date != self.ctx.data_out_of_date
or self.first_check
)
self.last_shown_tooltip = shown_tooltip
if not needs_redraw:
return
assert self.campaign_panel is not None
self.refresh_from_launching = True
self.clear_tooltip()
self.campaign_panel.clear_widgets()
if self.ctx.data_out_of_date:
self.campaign_panel.add_widget(Label(text="", padding=[0, 5, 0, 5]))
warning_label = DownloadDataWarningMessage(
text="Map/Mod data is out of date. Run /download_data in the client",
padding=[0, 25, 0, 25],
)
self.campaign_scroll_panel.border_on = True
self.campaign_panel.add_widget(warning_label)
else:
self.campaign_scroll_panel.border_on = False
self.last_data_out_of_date = self.ctx.data_out_of_date
if len(self.ctx.custom_mission_order) == 0:
self.campaign_panel.add_widget(Label(text="Connect to a world to see a mission layout here."))
return
self.last_checked_locations = self.ctx.checked_locations.copy()
self.last_items_received = sorted_items_received
self.first_check = False
self.mission_buttons = []
available_missions, available_layouts, available_campaigns, unfinished_missions = calc_unfinished_nodes(self.ctx)
# The MultiCampaignLayout widget needs a default height of 15 (set in the .kv) to display the above Labels correctly
multi_campaign_layout_height = 15
# Fetching IDs of all the locations with hints
self.hints_to_highlight = []
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}")
if hints:
for hint in hints:
if hint['finding_player'] == self.ctx.slot and not hint['found']:
self.hints_to_highlight.append(hint['location'])
MISSION_BUTTON_HEIGHT = 50
MISSION_BUTTON_PADDING = 6
for campaign_idx, campaign in enumerate(self.ctx.custom_mission_order):
longest_column = max(len(col) for layout in campaign.layouts for col in layout.missions)
if longest_column == 1:
campaign_layout_height = 115
else:
campaign_layout_height = (longest_column + 2) * (MISSION_BUTTON_HEIGHT + MISSION_BUTTON_PADDING)
multi_campaign_layout_height += campaign_layout_height
campaign_layout = CampaignLayout(size_hint_y=None, height=campaign_layout_height)
campaign_layout.add_widget(
Label(text=campaign.name, size_hint_y=None, height=25, outline_width=1)
)
mission_layout = MissionLayout(padding=[10,0,10,0])
for layout_idx, layout in enumerate(campaign.layouts):
layout_panel = RegionLayout()
layout_panel.add_widget(
Label(text=layout.name, size_hint_y=None, height=25, outline_width=1))
column_panel = ColumnLayout()
for column in layout.missions:
category_panel = MissionCategory(padding=[3,MISSION_BUTTON_PADDING,3,MISSION_BUTTON_PADDING])
for mission in column:
mission_id = mission.mission_id
# Empty mission slots
if mission_id == -1:
column_spacer = Label(text='', size_hint_y=None, height=MISSION_BUTTON_HEIGHT)
category_panel.add_widget(column_spacer)
continue
mission_obj = lookup_id_to_mission[mission_id]
mission_finished = self.ctx.is_mission_completed(mission_id)
is_layout_exit = mission_id in layout.exits and not mission_finished
is_campaign_exit = mission_id in campaign.exits and not mission_finished
text, tooltip = self.mission_text(
self.ctx, mission_id, mission_obj,
layout_idx, is_layout_exit, layout.name,
campaign_idx, is_campaign_exit, campaign.name,
available_missions, available_layouts, available_campaigns, unfinished_missions
)
mission_button = MissionButton(text=text, size_hint_y=None, height=MISSION_BUTTON_HEIGHT)
mission_button.mission_id = mission_id
if mission_id in self.ctx.final_mission_ids:
mission_button.is_goal = True
if is_layout_exit or is_campaign_exit:
mission_button.is_exit = True
mission_race = mission_obj.race
if mission_race == SC2Race.ANY:
mission_race = mission_obj.campaign.race
race = campaign_race_exceptions.get(mission_obj, mission_race)
if race in self.button_colors:
mission_button.background_color = self.button_colors[race]
mission_button.tooltip_text = tooltip
mission_button.bind(on_press=self.mission_callback)
self.mission_buttons.append(mission_button)
category_panel.add_widget(mission_button)
# layout_panel.add_widget(Label(text=""))
column_panel.add_widget(category_panel)
layout_panel.add_widget(column_panel)
mission_layout.add_widget(layout_panel)
campaign_layout.add_widget(mission_layout)
self.campaign_panel.add_widget(campaign_layout)
self.campaign_panel.height = multi_campaign_layout_height
# For some reason the AP HoverBehavior won't send an enter event if a button spawns under the cursor,
# so manually send an enter event if a button is hovered immediately
for button in self.mission_buttons:
if button.hovered:
button.dispatch("on_enter")
break
def mission_text(
self, ctx: SC2Context, mission_id: int, mission_obj: SC2Mission,
layout_id: int, is_layout_exit: bool, layout_name: str, campaign_id: int, is_campaign_exit: bool, campaign_name: str,
available_missions: List[int], available_layouts: Dict[int, List[int]], available_campaigns: List[int],
unfinished_missions: List[int]
) -> Tuple[str, str]:
COLOR_MISSION_IMPORTANT = "6495ED" # blue
COLOR_MISSION_UNIMPORTANT = "A0BEF4" # lighter blue
COLOR_MISSION_CLEARED = "FFFFFF" # white
COLOR_MISSION_LOCKED = "A9A9A9" # gray
COLOR_PARENT_LOCKED = "848484" # darker gray
COLOR_MISSION_FINAL = "FFBC95" # orange
COLOR_MISSION_FINAL_LOCKED = "D0C0BE" # gray + orange
COLOR_FINAL_PARENT_LOCKED = "D0C0BE" # gray + orange
COLOR_FINAL_MISSION_REMINDER = "FF5151" # light red
COLOR_VICTORY_LOCATION = "FFC156" # gold
COLOR_TOOLTIP_DONE = "51FF51" # light green
COLOR_TOOLTIP_NOT_DONE = "FF5151" # light red
text = mission_obj.mission_name
tooltip: str = ""
remaining_locations, plando_locations, remaining_count = self.sort_unfinished_locations(mission_id)
campaign_locked = campaign_id not in available_campaigns
layout_locked = layout_id not in available_layouts[campaign_id]
# Map has uncollected locations
if mission_id in unfinished_missions:
if self.any_valuable_locations(remaining_locations):
text = f"[color={COLOR_MISSION_IMPORTANT}]{text}[/color]"
else:
text = f"[color={COLOR_MISSION_UNIMPORTANT}]{text}[/color]"
elif mission_id in available_missions:
text = f"[color={COLOR_MISSION_CLEARED}]{text}[/color]"
# Map requirements not met
else:
mission_rule, layout_rule, campaign_rule = ctx.mission_id_to_entry_rules[mission_id]
mission_has_rule = mission_rule.amount > 0
layout_has_rule = layout_rule.amount > 0
extra_reqs = False
if campaign_locked:
text = f"[color={COLOR_PARENT_LOCKED}]{text}[/color]"
tooltip += "To unlock this campaign, "
shown_rule = campaign_rule
extra_reqs = layout_has_rule or mission_has_rule
elif layout_locked:
text = f"[color={COLOR_PARENT_LOCKED}]{text}[/color]"
tooltip += "To unlock this questline, "
shown_rule = layout_rule
extra_reqs = mission_has_rule
else:
text = f"[color={COLOR_MISSION_LOCKED}]{text}[/color]"
tooltip += "To unlock this mission, "
shown_rule = mission_rule
rule_tooltip = shown_rule.tooltip(0, lookup_id_to_mission, COLOR_TOOLTIP_DONE, COLOR_TOOLTIP_NOT_DONE)
tooltip += rule_tooltip.replace(rule_tooltip[0], rule_tooltip[0].lower(), 1)
extra_word = "are"
if shown_rule.shows_single_rule():
extra_word = "is"
tooltip += "."
if extra_reqs:
tooltip += f"\nThis mission has additional requirements\nthat will be shown once the above {extra_word} met."
# Mark exit missions
exit_for: str = ""
if is_layout_exit:
exit_for += layout_name if layout_name else "this questline"
if is_campaign_exit:
if exit_for:
exit_for += " and "
exit_for += campaign_name if campaign_name else "this campaign"
if exit_for:
if tooltip:
tooltip += "\n\n"
tooltip += f"Required to beat {exit_for}"
# Mark goal missions
if mission_id in self.ctx.final_mission_ids:
if mission_id in available_missions:
text = f"[color={COLOR_MISSION_FINAL}]{mission_obj.mission_name}[/color]"
elif campaign_locked or layout_locked:
text = f"[color={COLOR_FINAL_PARENT_LOCKED}]{mission_obj.mission_name}[/color]"
else:
text = f"[color={COLOR_MISSION_FINAL_LOCKED}]{mission_obj.mission_name}[/color]"
if tooltip and not exit_for:
tooltip += "\n\n"
elif exit_for:
tooltip += "\n"
if any(location_type == LocationType.VICTORY for (location_type, _, _) in remaining_locations):
tooltip += f"[color={COLOR_FINAL_MISSION_REMINDER}]Required to beat the world[/color]"
else:
tooltip += "This goal mission is already beaten.\nBeat the remaining goal missions to beat the world."
# Populate remaining location list
if remaining_count > 0:
if tooltip:
tooltip += "\n\n"
tooltip += f"[b][color={COLOR_MISSION_IMPORTANT}]Uncollected locations[/color][/b]"
last_location_type = LocationType.VICTORY
victory_printed = False
if self.ctx.mission_order_scouting != MissionOrderScouting.option_none:
mission_available = mission_id in available_missions
scoutable = self.is_scoutable(remaining_locations, mission_available, layout_locked, campaign_locked)
else:
scoutable = False
for location_type, location_name, _ in remaining_locations:
if location_type in (LocationType.VICTORY, LocationType.VICTORY_CACHE) and victory_printed:
continue
if location_type != last_location_type:
tooltip += f"\n[color={COLOR_MISSION_IMPORTANT}]{self.get_location_type_title(location_type)}:[/color]"
last_location_type = location_type
if location_type == LocationType.VICTORY:
victory_count = len([loc for loc in remaining_locations if loc[0] in (LocationType.VICTORY, LocationType.VICTORY_CACHE)])
victory_loc = location_name.replace(":", f":[color={COLOR_VICTORY_LOCATION}]")
if victory_count > 1:
victory_loc += f' ({victory_count})'
tooltip += f"\n- {victory_loc}[/color]"
victory_printed = True
else:
tooltip += f"\n- {location_name}"
if scoutable:
tooltip += self.handle_scout_display(location_name)
if len(plando_locations) > 0:
tooltip += "\n[b]Plando:[/b]\n- "
tooltip += "\n- ".join(plando_locations)
tooltip = f"[b]{text}[/b]\n" + tooltip
#If the mission has any hints pointing to a check, add asterisks around the mission name
if any(tuple(x in self.hints_to_highlight for x in self.ctx.locations_for_mission_id(mission_id))):
text = "* " + text + " *"
return text, tooltip
def mission_callback(self, button: MissionButton) -> None:
if button.last_touch.button == 'right':
self.open_mission_menu(button)
return
if not self.launching:
mission_id: int = button.mission_id
if self.ctx.play_mission(mission_id):
self.launching = mission_id
Clock.schedule_once(self.finish_launching, 10)
def open_mission_menu(self, button: MissionButton) -> None:
# Will be assigned later, used to close menu in callbacks
menu = None
mission_id = button.mission_id
def copy_mission_name():
Clipboard.copy(lookup_id_to_mission[mission_id].mission_name)
menu.dismiss()
menu_items = [
{
"text": "Copy Mission Name",
"on_release": copy_mission_name,
}
]
width_override = None
hinted_item_ids = Counter()
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}")
if hints:
for hint in hints:
if hint['receiving_player'] == self.ctx.slot and not hint['found']:
hinted_item_ids[hint['item']] += 1
if not self.ctx.is_mission_completed(mission_id) and not is_mission_available(self.ctx, mission_id):
# Uncompleted and inaccessible missions can have items hinted if they're needed
# The inaccessible restriction is to ensure users don't waste hints on missions that they can already access
items_needed = self.resolve_items_needed(mission_id)
received_items = compute_received_items(self.ctx)
for item_id, amount in items_needed.items():
# If we have already received or hinted enough of this item, skip it
if received_items[item_id] + hinted_item_ids[item_id] >= amount:
continue
if width_override is None:
width_override = dp(500)
item_name = self.ctx.item_names.lookup_in_game(item_id)
label_text = f"Hint Required Item: {item_name}"
def hint_and_close():
self.ctx.command_processor(self.ctx)(f"!hint {item_name}")
menu.dismiss()
menu_items.append({
"text": label_text,
"on_release": hint_and_close,
})
menu = MDDropdownMenu(
caller=button,
items=menu_items,
**({"width": width_override} if width_override else {}),
)
menu.open()
def resolve_items_needed(self, mission_id: int) -> Counter[int]:
def resolve_rule_to_items(rule: RuleData) -> Counter[int]:
if isinstance(rule, SubRuleRuleData):
all_items = Counter()
for sub_rule in rule.sub_rules:
# Take max of each item across all sub-rules
all_items |= resolve_rule_to_items(sub_rule)
return all_items
elif isinstance(rule, ItemRuleData):
return Counter(rule.item_ids)
else:
return Counter()
rules = self.ctx.mission_id_to_entry_rules[mission_id]
# Take max value of each item across all rules using '|'
return (resolve_rule_to_items(rules.mission_rule) |
resolve_rule_to_items(rules.layout_rule) |
resolve_rule_to_items(rules.campaign_rule))
def finish_launching(self, dt):
self.launching = False
def sort_unfinished_locations(self, mission_id: int) -> Tuple[List[Tuple[LocationType, str, int]], List[str], int]:
locations: List[Tuple[LocationType, str, int]] = []
location_name_to_index: Dict[str, int] = {}
for loc in self.ctx.locations_for_mission_id(mission_id):
if loc in self.ctx.missing_locations:
location_name = self.ctx.location_names.lookup_in_game(loc)
location_name_to_index[location_name] = len(locations)
locations.append((
lookup_location_id_to_type[loc],
location_name,
loc,
))
count = len(locations)
plando_locations = []
elements_to_remove: Set[Tuple[LocationType, str, int]] = set()
for plando_loc_name in self.ctx.plando_locations:
if plando_loc_name in location_name_to_index:
elements_to_remove.add(locations[location_name_to_index[plando_loc_name]])
plando_locations.append(plando_loc_name)
for element in elements_to_remove:
locations.remove(element)
return sorted(locations), plando_locations, count
def any_valuable_locations(self, locations: List[Tuple[LocationType, str, int]]) -> bool:
for location_type, _, location_id in locations:
if (self.ctx.location_inclusions[location_type] == LocationInclusion.option_enabled
and all(
self.ctx.location_inclusions_by_flag[flag] == LocationInclusion.option_enabled
for flag in lookup_location_id_to_flags[location_id].values()
)
):
return True
return False
def get_location_type_title(self, location_type: LocationType) -> str:
title = location_type.name.title().replace("_", " ")
if self.ctx.location_inclusions[location_type] == LocationInclusion.option_disabled:
title += " (Nothing)"
elif self.ctx.location_inclusions[location_type] == LocationInclusion.option_filler:
title += " (Filler)"
else:
title += ""
return title
def is_scoutable(self, remaining_locations, mission_available: bool, layout_locked: bool, campaign_locked: bool) -> bool:
if self.ctx.mission_order_scouting == MissionOrderScouting.option_all:
return True
elif self.ctx.mission_order_scouting == MissionOrderScouting.option_campaign and not campaign_locked:
return True
elif self.ctx.mission_order_scouting == MissionOrderScouting.option_layout and not layout_locked:
return True
elif self.ctx.mission_order_scouting == MissionOrderScouting.option_available and mission_available:
return True
elif self.ctx.mission_order_scouting == MissionOrderScouting.option_completed and len([loc for loc in remaining_locations if loc[0] in (LocationType.VICTORY, LocationType.VICTORY_CACHE)]) == 0:
# Assuming that when a mission is completed, all victory location are removed
return True
else:
return False
def handle_scout_display(self, location_name: str) -> str:
if self.ctx.mission_item_classification is None:
return ""
# Only one information is provided for the victory locations of a mission
if " Cache (" in location_name:
location_name = location_name.split(" Cache")[0]
item_classification_key = self.ctx.mission_item_classification[location_name]
if ((ItemClassification.progression & item_classification_key)
and (ItemClassification.useful & item_classification_key)
):
# Uncommon, but some games do this to show off that an item is super-important
# This can also happen on a victory display if the cache holds both progression and useful
return " [color=AF99EF](Useful+Progression)[/color]"
if ItemClassification.progression & item_classification_key:
return " [color=AF99EF](Progression)[/color]"
if ItemClassification.useful & item_classification_key:
return " [color=6D8BE8](Useful)[/color]"
if SC2World.settings.show_traps and ItemClassification.trap & item_classification_key:
return " [color=FA8072](Trap)[/color]"
return " [color=00EEEE](Filler)[/color]"
def start_gui(context: SC2Context):
context.ui = SC2Manager(context)
context.ui_task = asyncio.create_task(context.ui.async_run(), name="UI")
import pkgutil
data = pkgutil.get_data(SC2World.__module__, "starcraft2.kv").decode()
Builder.load_string(data)

View File

@@ -1,19 +1,66 @@
# Contributors
Contibutors are listed with preferred or Discord names first, with github usernames prepended with an `@`
Contributors are listed with preferred or Discord names first, with GitHub usernames prepended with an `@`.
Within an update, contributors for earlier sections are not repeated for their contributions in later sections;
code contributors also reported bugs and participated in beta testing.
## Update 2024.0
## Update 2025
### Code Changes
* Ziktofel (@Ziktofel)
* Salzkorn (@Salzkorn)
* EnvyDragon (@EnvyDragon)
* Phanerus (@MatthewMarinets)
* Phaneros (@MatthewMarinets)
* Magnemania (@Magnemania)
* Bones (@itsjustbones)
* Gemster (@Gemster312)
* SirChuckOfTheChuckles (@SirChuckOfTheChuckles)
* Snarky (@Snarky)
* MindHawk (@MindHawk)
* Cristall (@Cristall)
* WaikinDN (@WaikinDN)
* blorp77 (@blorp77)
* Dikhovinka (@AYaroslavskiy91)
* Subsourian (@Subsourian)
### Additional Assets
* Alice Voltaire
### Voice Acting
@-handles in this section are social media contacts rather than specifically GitHub in this section.
* Subsourian (@Subsourian) - Signifier, Slayer
* GiantGrantGames (@GiantGrantGames) - Trireme
* Phaneros (@MatthewMarinets)- Skirmisher
* Durygathn - Dawnbringer
* 7thAce (@7thAce) - Pulsar
* Panicmoon (@panicmoon.bsky.social) - Skylord
* JayborinoPlays (@Jayborino) - Oppressor
## Maintenance of 2024 release
* Ziktofel (@Ziktofel)
* Phaneros (@MatthewMarinets)
* Salzkorn (@Salzkorn)
* neocerber (@neocerber)
* Alchav (@Alchav)
* Berserker (@Berserker66)
* Exempt-Medic (@Exempt-Medic)
And many members of the greater Archipelago community for core changes that affected the StarCraft 2 apworld.
## Update 2024
### Code Changes
* Ziktofel (@Ziktofel)
* Salzkorn (@Salzkorn)
* EnvyDragon (@EnvyDragon)
* Phaneros (@MatthewMarinets)
* Madi Sylveon (@MadiMadsen)
* Magnemania (@Magnemania)
* Subsourian (@Subsourian)
* neocerber (@neocerber)
* Hopop (@hopop201)
* Alice Voltaire (@AliceVoltaire)
* Genderdruid (@ArchonofFail)
* CrazedCollie (@FoxOfWar)
* Bones (@itsjustbones)
### Additional Beta testing and bug reports
* Varcklen (@Varcklen)

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ The following unlocks are randomized as items:
1. Your ability to build any non-worker unit.
2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain
choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss!
3. Your ability to get the generic unit upgrades, such as attack and armour upgrades.
3. Your ability to get the generic unit upgrades, such as attack and armor upgrades.
4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades
for Zerg, and Spear of Adun upgrades for Protoss.
5. Small boosts to your starting mineral, vespene gas, and supply totals on each mission.
@@ -94,22 +94,31 @@ Will overwrite existing files
* Run without arguments to list all factions and colors that are available.
* `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation.
* Run without arguments to list all options.
* Run without `option_value` to check the current value of the option
* Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource
amounts, controlling AI allies, etc.
* `/disable_mission_check` Disables the check to see if a mission is available to play.
Meant for co-op runs where one player can play the next mission in a chain the other player is doing.
* `/play [mission_id]` Starts a StarCraft 2 mission based off of the mission_id provided
* `/available` Get what missions are currently available to play
* `/unfinished` Get what missions are currently available to play and have not had all locations checked
* `/set_path [path]` Manually set the SC2 install directory (if the automatic detection fails)
* `/windowed_mode [true|false]` to toggle whether the game will start in windowed mode.
Note that the behavior of the command `/received` was modified in the StarCraft 2 client.
In the Common client of Archipelago, the command returns the list of items received in the reverse order they were
received.
In the StarCraft 2 client, the returned list will be divided by races (i.e., Any, Protoss, Terran, and Zerg).
Additionally, upgrades are grouped beneath their corresponding units or buildings.
A filter parameter can be provided, e.g., `/received Thor`, to limit the number of items shown.
Every item whose name, race, or group name contains the provided parameter will be shown.
* In the Common client of Archipelago, the command returns the list of items received in the reverse order they were
received.
* In the StarCraft 2 client, the returned list will be divided by races (i.e., Any, Protoss, Terran, and Zerg).
Additionally, upgrades are grouped beneath their corresponding units or buildings.
* A filter parameter can be provided, e.g., `/received Thor`, to limit the number of items shown.
* Every item whose name, race, or group name contains the provided parameter will be shown.
* Use `/received recent [amount]` to display the last `amount` items received in chronological order
* `amount` defaults to 20 if not specified
## Client-side settings
Some settings can be set or overridden on the client side rather than within a world's options.
This can allow, for example, overriding difficulty to always be `hard` no matter what the world specified.
It can also modify display properties, like the client's window size on startup or the launcher button colours.
Modify these within the `sc2_options` section of the host.yaml file within the Archipelago directory.
## Particularities in a multiworld
@@ -118,9 +127,9 @@ Every item whose name, race, or group name contains the provided parameter will
One of the default options of multiworlds is that once a world has achieved its goal, it collects its items from all
other worlds.
If you do not want this to happen, you should ask the person generating the multiworld to set the `Collect Permission`
option to something else, e.g., manual.
option to something else, such as "Manual" or "Allow on goal completion."
If the generation is not done via the website, the person that does the generation should modify the `collect_mode`
option in their `host.yaml` file prior to generation.
option in their `host.yaml` file prior to generation.
If the multiworld has already been generated, the host can use the command `/option collect_mode [value]` to change
this option.
@@ -135,4 +144,6 @@ This does not affect the game and can be ignored.
- Currently, the StarCraft 2 client uses the Victory locations to determine which missions have been completed.
As a result, the Archipelago collect feature can sometime grant access to missions that are connected to a mission that
you did not complete.
- If all victory locations are collected in this manner, victory is not sent until the player replays a final mission
and recollects the victory location.

View File

@@ -112,10 +112,6 @@ supplémentaires données au début des missions, la capacité de contrôler les
* `/disable_mission_check` Désactive les requit pour lancer les missions.
Cette option a pour but de permettre de jouer en mode coopératif en permettant à un joueur de jouer à la prochaine
mission de la chaîne qu'un autre joueur est en train d'entamer.
* `/play [mission_id]` Lance la mission correspondant à l'identifiant donné.
* `/available` Affiche les missions qui sont présentement accessibles.
* `/unfinished` Affiche les missions qui sont présentement accessibles et dont certains des objectifs permettant
l'accès à un *item* n'ont pas été accomplis.
* `/set_path [path]` Permet de définir manuellement où *StarCraft 2* est installé ce qui est pertinent seulement si la
détection automatique de cette dernière échoue.
@@ -151,4 +147,4 @@ Cela n'affecte pas le jeu et peut être ignoré.
- Actuellement, le client de *StarCraft 2* utilise la *location* associée à la victoire d'une mission pour déterminer
si celle-ci a été complétée.
En conséquence, la fonctionnalité *collect* d'*Archipelago* peut rendre accessible des missions connectées à une
mission que vous n'avez pas terminée.
mission que vous n'avez pas terminée.

View File

@@ -62,69 +62,128 @@ If the Progression Balancing of one world is greater than that of others, items
obtained early, and vice versa if its value is smaller.
However, StarCraft 2 is more permissive regarding the items that can be used to progress, so this option has little
influence on progression in a StarCraft 2 world.
StarCraft 2.
Since this option increases the time required to generate a MultiWorld, we recommend deactivating it (i.e., setting it
to zero) for a StarCraft 2 world.
#### How do I specify items in a list, like in excluded items?
#### What does Tactics Level do?
Tactics level allows controlling the difficulty through what items you're likely to get early.
This is independent of game difficulty like causal, normal, hard, or brutal.
"Standard" and "Advanced" levels are guaranteed to be beatable with the items you are given.
The logic is a little more restrictive than a player's creativity, so an advanced player is likely to have
more items than they need in any situation. These levels are entirely safe to use in a multiworld.
The "Any Units" level only guarantees that a minimum number of faction-appropriate units or buildings are reachable
early on, with minimal restrictions on what those units are.
Generation will guarantee a number of faction-appropriate units are reachable before starting a mission,
based on the depth of that mission. For example, if the third mission is a zerg mission, it is guaranteed that 2
zerg units are somewhere in the preceding 2 missions. This logic level is not guaranteed to be beatable, and may
require lowering the difficulty level (`/difficulty` in the client) if many no-build missions are excluded.
The "No Logic" level provides no logical safeguards for beatability. It is only safe to use in a multiworld if the player curates
a start inventory or the organizer is okay with the possibility of the StarCraft 2 world being unbeatable.
Safeguards exist so that other games' items placed in the StarCraft 2 world are reachable under "Advanced" logic rules.
#### How do I specify items in a list, like in enabled campaigns?
You can look up the syntax for yaml collections in the
[YAML specification](https://yaml.org/spec/1.2.2/#21-collections).
For lists, every item goes on its own line, started with a hyphen:
For lists, every item goes on its own line, started with a hyphen.
Putting each element on its own line makes it easy to toggle elements by commenting
(ie adding a `#` character at the start of the line).
```yaml
excluded_items:
- Battlecruiser
- Drop-Pods (Kerrigan Tier 7)
enabled_campaigns:
- Wings of Liberty
# - Heart of the Swarm
- Legacy of the Void
- Nova Covert Ops
- Prophecy
- 'Whispers of Oblivion (Legacy of the Void: Prologue)'
# - 'Into the Void (Legacy of the Void: Epilogue)'
```
An inline syntax may also be used for short lists:
```yaml
enabled_campaigns: ['Wings of Liberty', 'Nova Covert Ops']
```
An empty list is just a matching pair of square brackets: `[]`.
That's the default value in the template, which should let you know to use this syntax.
That's often the default value in the template, which should let you know to use this syntax.
#### How do I specify items for the starting inventory?
#### How do I specify items for key-value mappings, like starting inventory or filler item distribution?
The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with.
The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value:
Many options pertaining to the item pool are yaml mappings.
These are several lines, where each line looks like a name, followed by a colon, then a space, then a value.
```yaml
start_inventory:
Micro-Filtering: 1
Additional Starting Vespene: 5
start_inventory:
Micro-Filtering: 1
Additional Starting Vespene: 5
locked_items:
MULE (Command Center): 1
```
For options like `start_inventory`, `locked_items`, `excluded_items`, and `unexcluded_items`, the value
is a number specifying how many copies of an item to start with/exclude/lock.
Note the name can also be an item group, and the value will then be added to the values for all the items
within the group. A value of `0` will exclude all copies of an item, but will add +0 if the value
is also specified by another name.
For options like `filler_items_distribution`, the value is a number specifying the relative weight of
a filler item being that particular item.
For the `custom_mission_order` option, the value is a nested structure of other mapppings to specify the structure
of the mission order. See the [Custom Mission Order documentation](/tutorial/Starcraft%202/custom_mission_orders_en)
An empty mapping is just a matching pair of curly braces: `{}`.
That's the default value in the template, which should let you know to use this syntax.
#### How do I know the exact names of items and locations?
The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations
for each game that it currently supports, including StarCraft 2.
You can also look up a complete list of the item names in the
You can look up a complete list of the item names in the
[Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page.
This page also contains supplementary information of each item.
However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the
former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development.
As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over
the mission in the 'StarCraft 2 Launcher' tab in the client.
Locations are of the format `<mission name>: <location name>`. Names are most easily looked up by hovering
your mouse over a mission in the launcher tab of a client. Note this requires already generating a game connect to.
This information can also be found in the [*datapackage*](/datapackage) page of the Archipelago website.
This page includes all data associated with all games.
## How do I join a MultiWorld game?
1. Run ArchipelagoStarcraft2Client.exe.
- macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step
only.
2. Type `/connect [server ip]`.
2. In the Archipelago tab, type `/connect [server IP]`.
- If you're running through the website, the server IP should be displayed near the top of the room page.
- The server IP may also be typed into the top bar, and then clicking "Connect"
3. Type your slot name from your YAML when prompted.
4. If the server has a password, enter that when prompted.
5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your
world.
Unreachable missions will have greyed-out text. Just click on an available mission to start it!
world.
Unreachable missions will have greyed-out text. Completed missions (all locations collected) will have white text.
Accessible but incomplete missions will have blue text. Goal missions will have a gold border.
Mission buttons will have a color corresponding to the faction you play as in that mission.
Click on an available mission to start it.
## The game isn't launching when I try to start a mission.
First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`).
Usually, this is caused by the mod files not being downloaded.
Make sure you have run `/download_data` in the Archipelago tab before playing.
You should only have to run `/download_data` again to pick up bugfixes and updates.
Make sure that you are running an up-to-date version of the client.
Check the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) to
look up what the latest version is (RC releases are not necessary; that stands for "Release Candidate").
If these things are in order, check the log file for issues (stored at `[Archipelago Directory]/logs/Starcraft2Client.txt`).
If you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel
for help.
Please include a specific description of what's going wrong and attach your log file to your message.
@@ -150,16 +209,15 @@ Note: to launch the client, you will need to run the command `python3 Starcraft2
## Running in Linux
To run StarCraft 2 through Archipelago in Linux, you will need to install the game using Wine, then run the Linux build
To run StarCraft 2 through Archipelago on Linux, you will need to install the game using Wine, then run the Linux build
of the Archipelago client.
Make sure you have StarCraft 2 installed using Wine, and that you have followed the
[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location.
You will not need to copy the `.dll` files.
If you're having trouble installing or running StarCraft 2 on Linux, it is recommend to use the Lutris installer.
Make sure you have StarCraft 2 installed using Wine, and you know where Wine and Starcraft 2 are installed.
If you're having trouble installing or running StarCraft 2 on Linux, it is recommended to use the Lutris installer.
Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables with the relevant
locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same
Copy the following into a .sh file, preferably within your Archipelago directory,
replacing the values of **WINE** and **SC2PATH** variables with the relevant locations,
as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same
folder as the script.
```sh
@@ -170,6 +228,13 @@ export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
# FIXME Replace with path to the version of Wine used to run SC2
export WINE="/usr/bin/wine"
# FIXME If using nondefault wineprefix for SC2 install (usual for Lutris installs), uncomment the next line and change the path
#export WINEPREFIX="/path/to/wineprefix"
# FIXME Uncomment the following lines if experiencing issues with DXVK (like DDRAW.ddl does not exist)
#export WINEDLLOVERRIDES=d3d10core,d3d11,d3d12,d3d12core,d3d9,d3dcompiler_33,d3dcompiler_34,d3dcompiler_35,d3dcompiler_36,d3dcompiler_37,d3dcompiler_38,d3dcompiler_39,d3dcompiler_40,d3dcompiler_41,d3dcompiler_42,d3dcompiler_43,d3dcompiler_46,d3dcompiler_47,d3dx10,d3dx10_33,d3dx10_34,d3dx10_35,d3dx10_36,d3dx10_37,d3dx10_38,d3dx10_39,d3dx10_40,d3dx10_41,d3dx10_42,d3dx10_43,d3dx11_42,d3dx11_43,d3dx9_24,d3dx9_25,d3dx9_26,d3dx9_27,d3dx9_28,d3dx9_29,d3dx9_30,d3dx9_31,d3dx9_32,d3dx9_33,d3dx9_34,d3dx9_35,d3dx9_36,d3dx9_37,d3dx9_38,d3dx9_39,d3dx9_40,d3dx9_41,d3dx9_42,d3dx9_43,dxgi,nvapi,nvapi64
#export DXVK_ENABLE_NVAPI=1
# FIXME Replace with path to StarCraft II install folder
export SC2PATH="/home/user/Games/starcraft-ii/drive_c/Program Files (x86)/StarCraft II/"
@@ -193,3 +258,6 @@ below, replacing **${ID}** with the numerical ID.
This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path
to the Wine binary that Lutris uses.
You can then remove the line that runs the Battle.Net launcher and copy the code above into the existing script.
Finally, you can run the script to start your Archipelago client,
and it should be able to launch Starcraft 2 when you start a mission.

View File

@@ -87,7 +87,7 @@ Pour les listes, chaque *item* doit être sur sa propre ligne et doit être pré
```yaml
excluded_items:
- Battlecruiser
- Drop-Pods (Kerrigan Tier 7)
- Drop-Pods (Kerrigan Ability)
```
Une liste vide est représentée par une paire de crochets: `[]`.

98
worlds/sc2/gui_config.py Normal file
View File

@@ -0,0 +1,98 @@
"""
Import this before importing client_gui.py to set window defaults from world settings.
"""
from .settings import Starcraft2Settings
from typing import List, Tuple, Any
def get_window_defaults() -> Tuple[List[str], int, int]:
"""
Gets the window size options from the sc2 settings.
Returns a list of warnings to be printed once the GUI is started, followed by the window width and height
"""
from . import SC2World
# validate settings
warnings: List[str] = []
if isinstance(SC2World.settings.window_height, int) and SC2World.settings.window_height > 0:
window_height = SC2World.settings.window_height
else:
warnings.append(f"Invalid value for options.yaml key sc2_options.window_height: '{SC2World.settings.window_height}'. Expected a positive integer.")
window_height = Starcraft2Settings.window_height
if isinstance(SC2World.settings.window_width, int) and SC2World.settings.window_width > 0:
window_width = SC2World.settings.window_width
else:
warnings.append(f"Invalid value for options.yaml key sc2_options.window_width: '{SC2World.settings.window_width}'. Expected a positive integer.")
window_width = Starcraft2Settings.window_width
return warnings, window_width, window_height
def validate_color(color: Any, default: Tuple[float, float, float]) -> Tuple[Tuple[str, ...], Tuple[float, float, float]]:
if isinstance(color, int):
if color < 0:
return ('Integer color was negative; expected a value from 0 to 0xffffff',), default
return (), (
((color >> 8) & 0xff) / 255,
((color >> 4) & 0xff) / 255,
((color >> 0) & 0xff) / 255,
)
elif color == 'default':
return (), default
elif color == 'white':
return (), (0.9, 0.9, 0.9)
elif color == 'black':
return (), (0.0, 0.0, 0.0)
elif color == 'grey':
return (), (0.345, 0.345, 0.345)
elif color == 'red':
return (), (0.85, 0.2, 0.1)
elif color == 'orange':
return (), (1.0, 0.65, 0.37)
elif color == 'green':
return (), (0.24, 0.84, 0.55)
elif color == 'blue':
return (), (0.3, 0.4, 1.0)
elif color == 'pink':
return (), (0.886, 0.176, 0.843)
elif not isinstance(color, list):
return (f'Invalid type {type(color)}; expected 3-element list or integer',), default
elif len(color) != 3:
return (f'Wrong number of elements in color; expected 3, got {len(color)}',), default
result: List[float] = [0.0, 0.0, 0.0]
errors: List[str] = []
expected = 'expected a number from 0 to 1'
for index, element in enumerate(color):
if isinstance(element, int):
element = float(element)
if not isinstance(element, float):
errors.append(f'Invalid type {type(element)} at index {index}; {expected}')
continue
if element < 0:
errors.append(f'Negative element {element} at index {index}; {expected}')
continue
if element > 1:
errors.append(f'Element {element} at index {index} is greater than 1; {expected}')
result[index] = 1.0
continue
result[index] = element
return tuple(errors), tuple(result)
def get_button_color(race: str) -> Tuple[Tuple[str, ...], Tuple[float, float, float]]:
from . import SC2World
baseline_color = 0.345 # the button graphic is grey, with this value in each color channel
if race == 'TERRAN':
user_color: list = SC2World.settings.terran_button_color
default_color = (0.0838, 0.2898, 0.2346)
elif race == 'PROTOSS':
user_color = SC2World.settings.protoss_button_color
default_color = (0.345, 0.22425, 0.12765)
elif race == 'ZERG':
user_color = SC2World.settings.zerg_button_color
default_color = (0.18975, 0.2415, 0.345)
else:
user_color = [baseline_color, baseline_color, baseline_color]
default_color = (baseline_color, baseline_color, baseline_color)
errors, color = validate_color(user_color, default_color)
return errors, tuple(x / baseline_color for x in color)

173
worlds/sc2/item/__init__.py Normal file
View File

@@ -0,0 +1,173 @@
import enum
import typing
from dataclasses import dataclass
from typing import Optional, Union, Dict, Type
from BaseClasses import Item, ItemClassification
from ..mission_tables import SC2Race
class ItemFilterFlags(enum.IntFlag):
"""Removed > Start Inventory > Locked > Excluded > Requested > Culled"""
Available = 0
StartInventory = enum.auto()
Locked = enum.auto()
"""Used to flag items that are never allowed to be culled."""
LogicLocked = enum.auto()
"""Locked by item cull logic checks; logic-locked w/a upgrades may be removed if all parents are removed"""
Requested = enum.auto()
"""Soft-locked items by item count checks during item culling; may be re-added"""
Removed = enum.auto()
"""Marked for immediate removal"""
UserExcluded = enum.auto()
"""Excluded by the user; display an error message if failing to exclude"""
FilterExcluded = enum.auto()
"""Excluded by item filtering"""
Culled = enum.auto()
"""Soft-removed by the item culling"""
NonLocal = enum.auto()
Plando = enum.auto()
AllowedOrphan = enum.auto()
"""Used to flag items that shouldn't be filtered out with their parents"""
ForceProgression = enum.auto()
"""Used to flag items that aren't classified as progression by default"""
Unexcludable = StartInventory|Plando|Locked|LogicLocked
UnexcludableUpgrade = StartInventory|Plando|Locked
Uncullable = StartInventory|Plando|Locked|LogicLocked|Requested
Excluded = UserExcluded|FilterExcluded
RequestedOrBetter = StartInventory|Locked|LogicLocked|Requested
CulledOrBetter = Removed|Excluded|Culled
class StarcraftItem(Item):
game: str = "Starcraft 2"
filter_flags: ItemFilterFlags = ItemFilterFlags.Available
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int, filter_flags: ItemFilterFlags = ItemFilterFlags.Available):
super().__init__(name, classification, code, player)
self.filter_flags = filter_flags
class ItemTypeEnum(enum.Enum):
def __new__(cls, *args, **kwargs):
value = len(cls.__members__) + 1
obj = object.__new__(cls)
obj._value_ = value
return obj
def __init__(self, name: str, flag_word: int):
self.display_name = name
self.flag_word = flag_word
class TerranItemType(ItemTypeEnum):
Armory_1 = "Armory", 0
"""General Terran unit upgrades"""
Armory_2 = "Armory", 1
Armory_3 = "Armory", 2
Armory_4 = "Armory", 3
Armory_5 = "Armory", 4
Armory_6 = "Armory", 5
Armory_7 = "Armory", 6
Progressive = "Progressive Upgrade", 7
Laboratory = "Laboratory", 8
Upgrade = "Upgrade", 9
Unit = "Unit", 10
Building = "Building", 11
Mercenary = "Mercenary", 12
Nova_Gear = "Nova Gear", 13
Progressive_2 = "Progressive Upgrade", 14
Unit_2 = "Unit", 15
class ZergItemType(ItemTypeEnum):
Ability = "Ability", 0
"""Kerrigan abilities"""
Mutation_1 = "Mutation", 1
Strain = "Strain", 2
Morph = "Morph", 3
Upgrade = "Upgrade", 4
Mercenary = "Mercenary", 5
Unit = "Unit", 6
Level = "Level", 7
"""Kerrigan level packs"""
Primal_Form = "Primal Form", 8
Evolution_Pit = "Evolution Pit", 9
"""Zerg global economy upgrades, like automated extractors"""
Mutation_2 = "Mutation", 10
Mutation_3 = "Mutation", 11
Mutation_4 = "Mutation", 12
Progressive = "Progressive Upgrade", 13
Mutation_5 = "Mutation", 14
class ProtossItemType(ItemTypeEnum):
Unit = "Unit", 0
Unit_2 = "Unit", 1
Upgrade = "Upgrade", 2
Building = "Building", 3
Progressive = "Progressive Upgrade", 4
Spear_Of_Adun = "Spear of Adun", 5
Solarite_Core = "Solarite Core", 6
"""Protoss global effects, such as reconstruction beam or automated assimilators"""
Forge_1 = "Forge", 7
"""General Protoss unit upgrades"""
Forge_2 = "Forge", 8
"""General Protoss unit upgrades"""
Forge_3 = "Forge", 9
"""General Protoss unit upgrades"""
Forge_4 = "Forge", 10
"""General Protoss unit upgrades"""
Forge_5 = "Forge", 11
"""General Protoss unit upgrades"""
War_Council = "War Council", 12
War_Council_2 = "War Council", 13
ShieldRegeneration = "Shield Regeneration Group", 14
class FactionlessItemType(ItemTypeEnum):
Minerals = "Minerals", 0
Vespene = "Vespene", 1
Supply = "Supply", 2
MaxSupply = "Max Supply", 3
BuildingSpeed = "Building Speed", 4
Nothing = "Nothing Group", 5
Deprecated = "Deprecated", 6
MaxSupplyTrap = "Max Supply Trap", 7
ResearchSpeed = "Research Speed", 8
ResearchCost = "Research Cost", 9
Keys = "Keys", -1
ItemType = Union[TerranItemType, ZergItemType, ProtossItemType, FactionlessItemType]
race_to_item_type: Dict[SC2Race, Type[ItemTypeEnum]] = {
SC2Race.ANY: FactionlessItemType,
SC2Race.TERRAN: TerranItemType,
SC2Race.ZERG: ZergItemType,
SC2Race.PROTOSS: ProtossItemType,
}
class ItemData(typing.NamedTuple):
code: int
type: ItemType
number: int # Important for bot commands to send the item into the game
race: SC2Race
classification: ItemClassification = ItemClassification.useful
quantity: int = 1
parent: typing.Optional[str] = None
important_for_filtering: bool = False
def is_important_for_filtering(self):
return (
self.important_for_filtering
or self.classification == ItemClassification.progression
or self.classification == ItemClassification.progression_skip_balancing
)
@dataclass
class FilterItem:
name: str
data: ItemData
index: int = 0
flags: ItemFilterFlags = ItemFilterFlags.Available

View File

@@ -0,0 +1,178 @@
"""
Annotations to add to item names sent to the in-game message panel
"""
from . import item_names
ITEM_NAME_ANNOTATIONS = {
item_names.MARINE: "(Barracks)",
item_names.MEDIC: "(Barracks)",
item_names.FIREBAT: "(Barracks)",
item_names.MARAUDER: "(Barracks)",
item_names.REAPER: "(Barracks)",
item_names.HELLION: "(Factory)",
item_names.VULTURE: "(Factory)",
item_names.GOLIATH: "(Factory)",
item_names.DIAMONDBACK: "(Factory)",
item_names.SIEGE_TANK: "(Factory)",
item_names.MEDIVAC: "(Starport)",
item_names.WRAITH: "(Starport)",
item_names.VIKING: "(Starport)",
item_names.BANSHEE: "(Starport)",
item_names.BATTLECRUISER: "(Starport)",
item_names.GHOST: "(Barracks)",
item_names.SPECTRE: "(Barracks)",
item_names.THOR: "(Factory)",
item_names.RAVEN: "(Starport)",
item_names.SCIENCE_VESSEL: "(Starport)",
item_names.PREDATOR: "(Factory)",
item_names.HERCULES: "(Starport)",
item_names.HERC: "(Barracks)",
item_names.DOMINION_TROOPER: "(Barracks)",
item_names.WIDOW_MINE: "(Factory)",
item_names.CYCLONE: "(Factory)",
item_names.WARHOUND: "(Factory)",
item_names.LIBERATOR: "(Starport)",
item_names.VALKYRIE: "(Starport)",
item_names.SON_OF_KORHAL: "(Elite Barracks)",
item_names.AEGIS_GUARD: "(Elite Barracks)",
item_names.FIELD_RESPONSE_THETA: "(Elite Barracks)",
item_names.EMPERORS_SHADOW: "(Elite Barracks)",
item_names.BULWARK_COMPANY: "(Elite Factory)",
item_names.SHOCK_DIVISION: "(Elite Factory)",
item_names.BLACKHAMMER: "(Elite Factory)",
item_names.SKY_FURY: "(Elite Starport)",
item_names.NIGHT_HAWK: "(Elite Starport)",
item_names.NIGHT_WOLF: "(Elite Starport)",
item_names.EMPERORS_GUARDIAN: "(Elite Starport)",
item_names.PRIDE_OF_AUGUSTRGRAD: "(Elite Starport)",
item_names.WAR_PIGS: "(Terran Mercenary)",
item_names.DEVIL_DOGS: "(Terran Mercenary)",
item_names.HAMMER_SECURITIES: "(Terran Mercenary)",
item_names.SPARTAN_COMPANY: "(Terran Mercenary)",
item_names.SIEGE_BREAKERS: "(Terran Mercenary)",
item_names.HELS_ANGELS: "(Terran Mercenary)",
item_names.DUSK_WINGS: "(Terran Mercenary)",
item_names.JACKSONS_REVENGE: "(Terran Mercenary)",
item_names.SKIBIS_ANGELS: "(Terran Mercenary)",
item_names.DEATH_HEADS: "(Terran Mercenary)",
item_names.WINGED_NIGHTMARES: "(Terran Mercenary)",
item_names.MIDNIGHT_RIDERS: "(Terran Mercenary)",
item_names.BRYNHILDS: "(Terran Mercenary)",
item_names.JOTUN: "(Terran Mercenary)",
item_names.BUNKER: "(Terran Building)",
item_names.MISSILE_TURRET: "(Terran Building)",
item_names.SENSOR_TOWER: "(Terran Building)",
item_names.PLANETARY_FORTRESS: "(Terran Building)",
item_names.PERDITION_TURRET: "(Terran Building)",
item_names.DEVASTATOR_TURRET: "(Terran Building)",
item_names.PSI_DISRUPTER: "(Terran Building)",
item_names.HIVE_MIND_EMULATOR: "(Terran Building)",
item_names.ZERGLING: "(Larva)",
item_names.SWARM_QUEEN: "(Hatchery)",
item_names.ROACH: "(Larva)",
item_names.HYDRALISK: "(Larva)",
item_names.ABERRATION: "(Larva)",
item_names.MUTALISK: "(Larva)",
item_names.SWARM_HOST: "(Larva)",
item_names.INFESTOR: "(Larva)",
item_names.ULTRALISK: "(Larva)",
item_names.PYGALISK: "(Larva)",
item_names.CORRUPTOR: "(Larva)",
item_names.SCOURGE: "(Larva)",
item_names.BROOD_QUEEN: "(Larva)",
item_names.DEFILER: "(Larva)",
item_names.INFESTED_MARINE: "(Infested Barracks)",
item_names.INFESTED_SIEGE_TANK: "(Infested Factory)",
item_names.INFESTED_DIAMONDBACK: "(Infested Factory)",
item_names.BULLFROG: "(Infested Factory)",
item_names.INFESTED_BANSHEE: "(Infested Starport)",
item_names.INFESTED_LIBERATOR: "(Infested Starport)",
item_names.ZERGLING_BANELING_ASPECT: "(Zergling Morph)",
item_names.HYDRALISK_IMPALER_ASPECT: "(Hydralisk Morph)",
item_names.HYDRALISK_LURKER_ASPECT: "(Hydralisk Morph)",
item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT: "(Mutalisk/Corruptor Morph)",
item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT: "(Mutalisk/Corruptor Morph)",
item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT: "(Mutalisk/Corruptor Morph)",
item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT: "(Mutalisk/Corruptor Morph)",
item_names.ROACH_RAVAGER_ASPECT: "(Roach Morph)",
item_names.OVERLORD_OVERSEER_ASPECT: "(Overlord Morph)",
item_names.ROACH_PRIMAL_IGNITER_ASPECT: "(Roach Morph)",
item_names.ULTRALISK_TYRANNOZOR_ASPECT: "(Ultralisk Morph)",
item_names.INFESTED_MEDICS: "(Zerg Mercenary)",
item_names.INFESTED_SIEGE_BREAKERS: "(Zerg Mercenary)",
item_names.INFESTED_DUSK_WINGS: "(Zerg Mercenary)",
item_names.DEVOURING_ONES: "(Zerg Mercenary)",
item_names.HUNTER_KILLERS: "(Zerg Mercenary)",
item_names.TORRASQUE_MERC: "(Zerg Mercenary)",
item_names.HUNTERLING: "(Zerg Mercenary)",
item_names.YGGDRASIL: "(Zerg Mercenary)",
item_names.CAUSTIC_HORRORS: "(Zerg Mercenary)",
item_names.SPORE_CRAWLER: "(Zerg Building)",
item_names.SPINE_CRAWLER: "(Zerg Building)",
item_names.BILE_LAUNCHER: "(Zerg Building)",
item_names.INFESTED_BUNKER: "(Zerg Building)",
item_names.INFESTED_MISSILE_TURRET: "(Zerg Building)",
item_names.NYDUS_WORM: "(Nydus Network)",
item_names.ECHIDNA_WORM: "(Nydus Network)",
item_names.ZEALOT: "(Gateway, Aiur)",
item_names.CENTURION: "(Gateway, Nerazim)",
item_names.SENTINEL: "(Gateway, Purifier)",
item_names.SUPPLICANT: "(Gateway, Tal'darim)",
item_names.STALKER: "(Gateway, Nerazim)",
item_names.INSTIGATOR: "(Gateway, Purifier)",
item_names.SLAYER: "(Gateway, Tal'darim)",
item_names.SENTRY: "(Gateway, Aiur)",
item_names.ENERGIZER: "(Gateway, Purifier)",
item_names.HAVOC: "(Gateway, Tal'darim)",
item_names.HIGH_TEMPLAR: "(Gateway, Aiur)",
item_names.SIGNIFIER: "(Gateway, Nerazim)",
item_names.ASCENDANT: "(Gateway, Tal'darim)",
item_names.DARK_TEMPLAR: "(Gateway, Nerazim)",
item_names.AVENGER: "(Gateway, Aiur)",
item_names.BLOOD_HUNTER: "(Gateway, Tal'darim)",
item_names.DRAGOON: "(Gateway, Aiur)",
item_names.DARK_ARCHON: "(Gateway, Nerazim)",
item_names.ADEPT: "(Gateway, Purifier)",
item_names.OBSERVER: "(Robotics Facility)",
item_names.WARP_PRISM: "(Robotics Facility)",
item_names.IMMORTAL: "(Robotics Facility, Aiur)",
item_names.ANNIHILATOR: "(Robotics Facility, Nerazim)",
item_names.VANGUARD: "(Robotics Facility, Tal'darim)",
item_names.STALWART: "(Robotics Facility, Purifier)",
item_names.COLOSSUS: "(Robotics Facility, Purifier)",
item_names.WRATHWALKER: "(Robotics Facility, Tal'darim)",
item_names.REAVER: "(Robotics Facility, Aiur)",
item_names.DISRUPTOR: "(Robotics Facility, Purifier)",
item_names.PHOENIX: "(Stargate, Aiur)",
item_names.MIRAGE: "(Stargate, Purifier)",
item_names.SKIRMISHER: "(Stargate, Tal'darim)",
item_names.CORSAIR: "(Stargate, Nerazim)",
item_names.VOID_RAY: "(Stargate, Nerazim)",
item_names.DESTROYER: "(Stargate, Tal'darim)",
item_names.PULSAR: "(Stargate, Aiur)",
item_names.DAWNBRINGER: "(Stargate, Purifier)",
item_names.SCOUT: "(Stargate, Aiur)",
item_names.OPPRESSOR: "(Stargate, Tal'darim)",
item_names.CALADRIUS: "(Stargate, Purifier)",
item_names.MISTWING: "(Stargate, Nerazim)",
item_names.CARRIER: "(Stargate, Aiur)",
item_names.SKYLORD: "(Stargate, Tal'darim)",
item_names.TRIREME: "(Stargate, Purifier)",
item_names.TEMPEST: "(Stargate, Purifier)",
item_names.MOTHERSHIP: "(Stargate, Tal'darim)",
item_names.ARBITER: "(Stargate, Aiur)",
item_names.ORACLE: "(Stargate, Nerazim)",
item_names.PHOTON_CANNON: "(Protoss Building)",
item_names.KHAYDARIN_MONOLITH: "(Protoss Building)",
item_names.SHIELD_BATTERY: "(Protoss Building)",
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,902 @@
import typing
from . import item_tables, item_names
from .item_tables import key_item_table
from ..mission_tables import campaign_mission_table, SC2Campaign, SC2Mission, SC2Race
"""
Item name groups, given to Archipelago and used in YAMLs and /received filtering.
For non-developers the following will be useful:
* Items with a bracket get groups named after the unbracketed part
* eg. "Advanced Healing AI (Medivac)" is accessible as "Advanced Healing AI"
* The exception to this are item names that would be ambiguous (eg. "Resource Efficiency")
* Item flaggroups get unique groups as well as combined groups for numbered flaggroups
* eg. "Unit" contains all units, "Armory" contains "Armory 1" through "Armory 6"
* The best place to look these up is at the bottom of Items.py
* Items that have a parent are grouped together
* eg. "Zergling Items" contains all items that have "Zergling" as a parent
* These groups do NOT contain the parent item
* This currently does not include items with multiple potential parents, like some LotV unit upgrades
* All items are grouped by their race ("Terran", "Protoss", "Zerg", "Any")
* Hand-crafted item groups can be found at the bottom of this file
"""
item_name_groups: typing.Dict[str, typing.List[str]] = {}
# Groups for use in world logic
item_name_groups["Missions"] = ["Beat " + mission.mission_name for mission in SC2Mission]
item_name_groups["WoL Missions"] = ["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.WOL]] + \
["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.PROPHECY]]
# These item name groups should not show up in documentation
unlisted_item_name_groups = {
"Missions", "WoL Missions",
item_tables.TerranItemType.Progressive.display_name,
item_tables.TerranItemType.Nova_Gear.display_name,
item_tables.TerranItemType.Mercenary.display_name,
item_tables.ZergItemType.Ability.display_name,
item_tables.ZergItemType.Morph.display_name,
item_tables.ZergItemType.Strain.display_name,
}
# Some item names only differ in bracketed parts
# These items are ambiguous for short-hand name groups
bracketless_duplicates: typing.Set[str]
# This is a list of names in ItemNames with bracketed parts removed, for internal use
_shortened_names = [(name[:name.find(' (')] if '(' in name else name)
for name in [item_names.__dict__[name] for name in item_names.__dir__() if not name.startswith('_')]]
# Remove the first instance of every short-name from the full item list
bracketless_duplicates = set(_shortened_names)
for name in bracketless_duplicates:
_shortened_names.remove(name)
# The remaining short-names are the duplicates
bracketless_duplicates = set(_shortened_names)
del _shortened_names
# All items get sorted into their data type
for item, data in item_tables.get_full_item_list().items():
# Items get assigned to their flaggroup's display type
item_name_groups.setdefault(data.type.display_name, []).append(item)
# Items with a bracket get a short-hand name group for ease of use in YAMLs
if '(' in item:
short_name = item[:item.find(' (')]
# Ambiguous short-names are dropped
if short_name not in bracketless_duplicates:
item_name_groups[short_name] = [item]
# Short-name groups are unlisted
unlisted_item_name_groups.add(short_name)
# Items with a parent get assigned to their parent's group
if data.parent:
# The parent groups need a special name, otherwise they are ambiguous with the parent
parent_group = f"{data.parent} Items"
item_name_groups.setdefault(parent_group, []).append(item)
# Parent groups are unlisted
unlisted_item_name_groups.add(parent_group)
# All items get assigned to their race's group
race_group = data.race.name.capitalize()
item_name_groups.setdefault(race_group, []).append(item)
# Hand-made groups
class ItemGroupNames:
TERRAN_ITEMS = "Terran Items"
"""All Terran items"""
TERRAN_UNITS = "Terran Units"
TERRAN_GENERIC_UPGRADES = "Terran Generic Upgrades"
"""+attack/armour upgrades"""
BARRACKS_UNITS = "Barracks Units"
FACTORY_UNITS = "Factory Units"
STARPORT_UNITS = "Starport Units"
WOL_UNITS = "WoL Units"
WOL_MERCS = "WoL Mercenaries"
WOL_BUILDINGS = "WoL Buildings"
WOL_UPGRADES = "WoL Upgrades"
WOL_ITEMS = "WoL Items"
"""All items from vanilla WoL. Note some items are progressive where level 2 is not vanilla."""
NCO_UNITS = "NCO Units"
NCO_BUILDINGS = "NCO Buildings"
NCO_UNIT_TECHNOLOGY = "NCO Unit Technology"
NCO_BASELINE_UPGRADES = "NCO Baseline Upgrades"
NCO_UPGRADES = "NCO Upgrades"
NOVA_EQUIPMENT = "Nova Equipment"
NOVA_WEAPONS = "Nova Weapons"
NOVA_GADGETS = "Nova Gadgets"
NCO_MAX_PROGRESSIVE_ITEMS = "NCO +Items"
"""NCO item groups that should be set to maximum progressive amounts"""
NCO_MIN_PROGRESSIVE_ITEMS = "NCO -Items"
"""NCO item groups that should be set to minimum progressive amounts (1)"""
TERRAN_BUILDINGS = "Terran Buildings"
TERRAN_MERCENARIES = "Terran Mercenaries"
TERRAN_STIMPACKS = "Terran Stimpacks"
TERRAN_PROGRESSIVE_UPGRADES = "Terran Progressive Upgrades"
TERRAN_ORIGINAL_PROGRESSIVE_UPGRADES = "Terran Original Progressive Upgrades"
"""Progressive items where level 1 appeared in WoL"""
MENGSK_UNITS = "Mengsk Units"
TERRAN_VETERANCY_UNITS = "Terran Veterancy Units"
ORBITAL_COMMAND_ABILITIES = "Orbital Command Abilities"
WOL_ORBITAL_COMMAND_ABILITIES = "WoL Command Center Abilities"
ZERG_ITEMS = "Zerg Items"
ZERG_UNITS = "Zerg Units"
ZERG_NONMORPH_UNITS = "Zerg Non-morph Units"
ZERG_GENERIC_UPGRADES = "Zerg Generic Upgrades"
"""+attack/armour upgrades"""
HOTS_UNITS = "HotS Units"
HOTS_BUILDINGS = "HotS Buildings"
HOTS_STRAINS = "HotS Strains"
"""Vanilla HotS strains (the upgrades you play a mini-mission for)"""
HOTS_MUTATIONS = "HotS Mutations"
"""Vanilla HotS Mutations (basic toggleable unit upgrades)"""
HOTS_GLOBAL_UPGRADES = "HotS Global Upgrades"
HOTS_MORPHS = "HotS Morphs"
KERRIGAN_ABILITIES = "Kerrigan Abilities"
KERRIGAN_HOTS_ABILITIES = "Kerrigan HotS Abilities"
KERRIGAN_ACTIVE_ABILITIES = "Kerrigan Active Abilities"
KERRIGAN_LOGIC_ACTIVE_ABILITIES = "Kerrigan Logic Active Abilities"
KERRIGAN_PASSIVES = "Kerrigan Passives"
KERRIGAN_TIER_1 = "Kerrigan Tier 1"
KERRIGAN_TIER_2 = "Kerrigan Tier 2"
KERRIGAN_TIER_3 = "Kerrigan Tier 3"
KERRIGAN_TIER_4 = "Kerrigan Tier 4"
KERRIGAN_TIER_5 = "Kerrigan Tier 5"
KERRIGAN_TIER_6 = "Kerrigan Tier 6"
KERRIGAN_TIER_7 = "Kerrigan Tier 7"
KERRIGAN_ULTIMATES = "Kerrigan Ultimates"
KERRIGAN_LOGIC_ULTIMATES = "Kerrigan Logic Ultimates"
KERRIGAN_NON_ULTIMATES = "Kerrigan Non-Ultimates"
KERRIGAN_NON_ULTIMATE_ACTIVE_ABILITIES = "Kerrigan Non-Ultimate Active Abilities"
HOTS_ITEMS = "HotS Items"
"""All items from vanilla HotS"""
OVERLORD_UPGRADES = "Overlord Upgrades"
ZERG_MORPHS = "Zerg Morphs"
ZERG_MERCENARIES = "Zerg Mercenaries"
ZERG_BUILDINGS = "Zerg Buildings"
INF_TERRAN_ITEMS = "Infested Terran Items"
"""All items from Stukov co-op subfaction"""
INF_TERRAN_UNITS = "Infested Terran Units"
INF_TERRAN_UPGRADES = "Infested Terran Upgrades"
PROTOSS_ITEMS = "Protoss Items"
PROTOSS_UNITS = "Protoss Units"
PROTOSS_GENERIC_UPGRADES = "Protoss Generic Upgrades"
"""+attack/armour upgrades"""
GATEWAY_UNITS = "Gateway Units"
ROBO_UNITS = "Robo Units"
STARGATE_UNITS = "Stargate Units"
PROPHECY_UNITS = "Prophecy Units"
PROPHECY_BUILDINGS = "Prophecy Buildings"
LOTV_UNITS = "LotV Units"
LOTV_ITEMS = "LotV Items"
LOTV_GLOBAL_UPGRADES = "LotV Global Upgrades"
SOA_ITEMS = "SOA"
PROTOSS_GLOBAL_UPGRADES = "Protoss Global Upgrades"
PROTOSS_BUILDINGS = "Protoss Buildings"
WAR_COUNCIL = "Protoss War Council Upgrades"
AIUR_UNITS = "Aiur"
NERAZIM_UNITS = "Nerazim"
TAL_DARIM_UNITS = "Tal'Darim"
PURIFIER_UNITS = "Purifier"
VANILLA_ITEMS = "Vanilla Items"
OVERPOWERED_ITEMS = "Overpowered Items"
UNRELEASED_ITEMS = "Unreleased Items"
LEGACY_ITEMS = "Legacy Items"
KEYS = "Keys"
@classmethod
def get_all_group_names(cls) -> typing.Set[str]:
return {
name for identifier, name in cls.__dict__.items()
if not identifier.startswith('_')
and not identifier.startswith('get_')
}
# Terran
item_name_groups[ItemGroupNames.TERRAN_ITEMS] = terran_items = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.race == SC2Race.TERRAN
]
item_name_groups[ItemGroupNames.TERRAN_UNITS] = terran_units = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type in (
item_tables.TerranItemType.Unit, item_tables.TerranItemType.Unit_2, item_tables.TerranItemType.Mercenary)
]
item_name_groups[ItemGroupNames.TERRAN_GENERIC_UPGRADES] = terran_generic_upgrades = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type == item_tables.TerranItemType.Upgrade
]
barracks_wa_group = [
item_names.MARINE, item_names.FIREBAT, item_names.MARAUDER,
item_names.REAPER, item_names.GHOST, item_names.SPECTRE, item_names.HERC, item_names.AEGIS_GUARD,
item_names.EMPERORS_SHADOW, item_names.DOMINION_TROOPER, item_names.SON_OF_KORHAL,
]
item_name_groups[ItemGroupNames.BARRACKS_UNITS] = barracks_units = (barracks_wa_group + [
item_names.MEDIC,
item_names.FIELD_RESPONSE_THETA,
])
factory_wa_group = [
item_names.HELLION, item_names.VULTURE, item_names.GOLIATH, item_names.DIAMONDBACK,
item_names.SIEGE_TANK, item_names.THOR, item_names.PREDATOR,
item_names.CYCLONE, item_names.WARHOUND, item_names.SHOCK_DIVISION, item_names.BLACKHAMMER,
item_names.BULWARK_COMPANY,
]
item_name_groups[ItemGroupNames.FACTORY_UNITS] = factory_units = (factory_wa_group + [
item_names.WIDOW_MINE,
])
starport_wa_group = [
item_names.WRAITH, item_names.VIKING, item_names.BANSHEE,
item_names.BATTLECRUISER, item_names.RAVEN_HUNTER_SEEKER_WEAPON,
item_names.LIBERATOR, item_names.VALKYRIE, item_names.PRIDE_OF_AUGUSTRGRAD, item_names.SKY_FURY,
item_names.EMPERORS_GUARDIAN, item_names.NIGHT_HAWK, item_names.NIGHT_WOLF,
]
item_name_groups[ItemGroupNames.STARPORT_UNITS] = starport_units = [
item_names.MEDIVAC, item_names.WRAITH, item_names.VIKING, item_names.BANSHEE,
item_names.BATTLECRUISER, item_names.HERCULES, item_names.SCIENCE_VESSEL, item_names.RAVEN,
item_names.LIBERATOR, item_names.VALKYRIE, item_names.PRIDE_OF_AUGUSTRGRAD, item_names.SKY_FURY,
item_names.EMPERORS_GUARDIAN, item_names.NIGHT_HAWK, item_names.NIGHT_WOLF,
]
item_name_groups[ItemGroupNames.TERRAN_MERCENARIES] = terran_mercenaries = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type == item_tables.TerranItemType.Mercenary
]
item_name_groups[ItemGroupNames.NCO_UNITS] = nco_units = [
item_names.MARINE, item_names.MARAUDER, item_names.REAPER,
item_names.HELLION, item_names.GOLIATH, item_names.SIEGE_TANK,
item_names.RAVEN, item_names.LIBERATOR, item_names.BANSHEE, item_names.BATTLECRUISER,
item_names.HERC, # From that one bonus objective in mission 5
]
item_name_groups[ItemGroupNames.NCO_BUILDINGS] = nco_buildings = [
item_names.BUNKER, item_names.MISSILE_TURRET, item_names.PLANETARY_FORTRESS,
]
item_name_groups[ItemGroupNames.NOVA_EQUIPMENT] = nova_equipment = [
*[item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type == item_tables.TerranItemType.Nova_Gear],
item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE,
]
item_name_groups[ItemGroupNames.NOVA_WEAPONS] = nova_weapons = [
item_names.NOVA_C20A_CANISTER_RIFLE,
item_names.NOVA_HELLFIRE_SHOTGUN,
item_names.NOVA_PLASMA_RIFLE,
item_names.NOVA_MONOMOLECULAR_BLADE,
item_names.NOVA_BLAZEFIRE_GUNBLADE,
]
item_name_groups[ItemGroupNames.NOVA_GADGETS] = nova_gadgets = [
item_names.NOVA_STIM_INFUSION,
item_names.NOVA_PULSE_GRENADES,
item_names.NOVA_FLASHBANG_GRENADES,
item_names.NOVA_IONIC_FORCE_FIELD,
item_names.NOVA_HOLO_DECOY,
]
item_name_groups[ItemGroupNames.WOL_UNITS] = wol_units = [
item_names.MARINE, item_names.MEDIC, item_names.FIREBAT, item_names.MARAUDER, item_names.REAPER,
item_names.HELLION, item_names.VULTURE, item_names.GOLIATH, item_names.DIAMONDBACK, item_names.SIEGE_TANK,
item_names.MEDIVAC, item_names.WRAITH, item_names.VIKING, item_names.BANSHEE, item_names.BATTLECRUISER,
item_names.GHOST, item_names.SPECTRE, item_names.THOR,
item_names.PREDATOR, item_names.HERCULES,
item_names.SCIENCE_VESSEL, item_names.RAVEN,
]
item_name_groups[ItemGroupNames.WOL_MERCS] = wol_mercs = [
item_names.WAR_PIGS, item_names.DEVIL_DOGS, item_names.HAMMER_SECURITIES,
item_names.SPARTAN_COMPANY, item_names.SIEGE_BREAKERS,
item_names.HELS_ANGELS, item_names.DUSK_WINGS, item_names.JACKSONS_REVENGE,
]
item_name_groups[ItemGroupNames.WOL_BUILDINGS] = wol_buildings = [
item_names.BUNKER, item_names.MISSILE_TURRET, item_names.SENSOR_TOWER,
item_names.PERDITION_TURRET, item_names.PLANETARY_FORTRESS,
item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER,
]
item_name_groups[ItemGroupNames.TERRAN_BUILDINGS] = terran_buildings = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type == item_tables.TerranItemType.Building or item_name in wol_buildings
]
item_name_groups[ItemGroupNames.MENGSK_UNITS] = [
item_names.AEGIS_GUARD, item_names.EMPERORS_SHADOW,
item_names.SHOCK_DIVISION, item_names.BLACKHAMMER,
item_names.PRIDE_OF_AUGUSTRGRAD, item_names.SKY_FURY,
item_names.DOMINION_TROOPER,
]
item_name_groups[ItemGroupNames.TERRAN_VETERANCY_UNITS] = [
item_names.AEGIS_GUARD, item_names.EMPERORS_SHADOW, item_names.SHOCK_DIVISION, item_names.BLACKHAMMER,
item_names.PRIDE_OF_AUGUSTRGRAD, item_names.SKY_FURY, item_names.SON_OF_KORHAL, item_names.FIELD_RESPONSE_THETA,
item_names.BULWARK_COMPANY, item_names.NIGHT_HAWK, item_names.EMPERORS_GUARDIAN, item_names.NIGHT_WOLF,
]
item_name_groups[ItemGroupNames.ORBITAL_COMMAND_ABILITIES] = orbital_command_abilities = [
item_names.COMMAND_CENTER_SCANNER_SWEEP,
item_names.COMMAND_CENTER_MULE,
item_names.COMMAND_CENTER_EXTRA_SUPPLIES,
]
item_name_groups[ItemGroupNames.WOL_ORBITAL_COMMAND_ABILITIES] = wol_orbital_command_abilities = [
item_names.COMMAND_CENTER_SCANNER_SWEEP,
item_names.COMMAND_CENTER_MULE,
]
spider_mine_sources = [
item_names.VULTURE,
item_names.REAPER_SPIDER_MINES,
item_names.SIEGE_TANK_SPIDER_MINES,
item_names.RAVEN_SPIDER_MINES,
]
# Terran Upgrades
item_name_groups[ItemGroupNames.WOL_UPGRADES] = wol_upgrades = [
# Armory Base
item_names.BUNKER_PROJECTILE_ACCELERATOR, item_names.BUNKER_NEOSTEEL_BUNKER,
item_names.MISSILE_TURRET_TITANIUM_HOUSING, item_names.MISSILE_TURRET_HELLSTORM_BATTERIES,
item_names.SCV_ADVANCED_CONSTRUCTION, item_names.SCV_DUAL_FUSION_WELDERS,
item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, item_names.COMMAND_CENTER_MULE, item_names.COMMAND_CENTER_SCANNER_SWEEP,
# Armory Infantry
item_names.MARINE_PROGRESSIVE_STIMPACK, item_names.MARINE_COMBAT_SHIELD,
item_names.MEDIC_ADVANCED_MEDIC_FACILITIES, item_names.MEDIC_STABILIZER_MEDPACKS,
item_names.FIREBAT_INCINERATOR_GAUNTLETS, item_names.FIREBAT_JUGGERNAUT_PLATING,
item_names.MARAUDER_CONCUSSIVE_SHELLS, item_names.MARAUDER_KINETIC_FOAM,
item_names.REAPER_U238_ROUNDS, item_names.REAPER_G4_CLUSTERBOMB,
# Armory Vehicles
item_names.HELLION_TWIN_LINKED_FLAMETHROWER, item_names.HELLION_THERMITE_FILAMENTS,
item_names.SPIDER_MINE_CERBERUS_MINE, item_names.VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE,
item_names.GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM, item_names.GOLIATH_ARES_CLASS_TARGETING_SYSTEM,
item_names.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL, item_names.DIAMONDBACK_SHAPED_HULL,
item_names.SIEGE_TANK_MAELSTROM_ROUNDS, item_names.SIEGE_TANK_SHAPED_BLAST,
# Armory Starships
item_names.MEDIVAC_RAPID_DEPLOYMENT_TUBE, item_names.MEDIVAC_ADVANCED_HEALING_AI,
item_names.WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS, item_names.WRAITH_DISPLACEMENT_FIELD,
item_names.VIKING_RIPWAVE_MISSILES, item_names.VIKING_PHOBOS_CLASS_WEAPONS_SYSTEM,
item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS, item_names.BANSHEE_SHOCKWAVE_MISSILE_BATTERY,
item_names.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS, item_names.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX,
# Armory Dominion
item_names.GHOST_OCULAR_IMPLANTS, item_names.GHOST_CRIUS_SUIT,
item_names.SPECTRE_PSIONIC_LASH, item_names.SPECTRE_NYX_CLASS_CLOAKING_MODULE,
item_names.THOR_330MM_BARRAGE_CANNON, item_names.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL,
# Lab Zerg
item_names.BUNKER_FORTIFIED_BUNKER, item_names.BUNKER_SHRIKE_TURRET,
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL, item_names.CELLULAR_REACTOR,
# Other 3 levels are units/buildings (Perdition, PF, Hercules, Predator, HME, Psi Disrupter)
# Lab Protoss
item_names.VANADIUM_PLATING, item_names.ULTRA_CAPACITORS,
item_names.AUTOMATED_REFINERY, item_names.MICRO_FILTERING,
item_names.ORBITAL_DEPOTS, item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR,
item_names.ORBITAL_STRIKE, item_names.TECH_REACTOR,
# Other level is units (Raven, Science Vessel)
]
item_name_groups[ItemGroupNames.TERRAN_STIMPACKS] = terran_stimpacks = [
item_names.MARINE_PROGRESSIVE_STIMPACK,
item_names.MARAUDER_PROGRESSIVE_STIMPACK,
item_names.REAPER_PROGRESSIVE_STIMPACK,
item_names.FIREBAT_PROGRESSIVE_STIMPACK,
item_names.HELLION_PROGRESSIVE_STIMPACK,
]
item_name_groups[ItemGroupNames.TERRAN_ORIGINAL_PROGRESSIVE_UPGRADES] = terran_original_progressive_upgrades = [
item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM,
item_names.MARINE_PROGRESSIVE_STIMPACK,
item_names.VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE,
item_names.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL,
item_names.WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS,
item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS,
item_names.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS,
item_names.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX,
item_names.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL,
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL,
]
item_name_groups[ItemGroupNames.NCO_BASELINE_UPGRADES] = nco_baseline_upgrades = [
item_names.BUNKER_NEOSTEEL_BUNKER, # Baseline from mission 2
item_names.BUNKER_FORTIFIED_BUNKER, # Baseline from mission 2
item_names.MARINE_COMBAT_SHIELD, # Baseline from mission 2
item_names.MARAUDER_KINETIC_FOAM, # Baseline outside WOL
item_names.MARAUDER_CONCUSSIVE_SHELLS, # Baseline from mission 2
item_names.REAPER_BALLISTIC_FLIGHTSUIT, # Baseline from mission 2
item_names.HELLION_HELLBAT, # Baseline from mission 3
item_names.GOLIATH_INTERNAL_TECH_MODULE, # Baseline from mission 4
item_names.GOLIATH_SHAPED_HULL,
# ItemNames.GOLIATH_RESOURCE_EFFICIENCY, # Supply savings baseline in NCO, mineral savings is non-NCO
item_names.SIEGE_TANK_SHAPED_HULL, # Baseline NCO gives +10; this upgrade gives +25
item_names.SIEGE_TANK_SHAPED_BLAST, # Baseline from mission 3
item_names.LIBERATOR_RAID_ARTILLERY, # Baseline in mission 5
item_names.RAVEN_BIO_MECHANICAL_REPAIR_DRONE, # Baseline in mission 5
item_names.BATTLECRUISER_TACTICAL_JUMP,
item_names.BATTLECRUISER_MOIRAI_IMPULSE_DRIVE,
item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, # Baseline from mission 2
item_names.ORBITAL_DEPOTS, # Baseline from mission 2
item_names.COMMAND_CENTER_SCANNER_SWEEP, # In NCO you must actually morph Command Center into Orbital Command
item_names.COMMAND_CENTER_EXTRA_SUPPLIES, # But in AP this works WoL-style
] + nco_buildings
item_name_groups[ItemGroupNames.NCO_UNIT_TECHNOLOGY] = nco_unit_technology = [
item_names.MARINE_LASER_TARGETING_SYSTEM,
item_names.MARINE_PROGRESSIVE_STIMPACK,
item_names.MARINE_MAGRAIL_MUNITIONS,
item_names.MARINE_OPTIMIZED_LOGISTICS,
item_names.MARAUDER_LASER_TARGETING_SYSTEM,
item_names.MARAUDER_INTERNAL_TECH_MODULE,
item_names.MARAUDER_PROGRESSIVE_STIMPACK,
item_names.MARAUDER_MAGRAIL_MUNITIONS,
item_names.REAPER_SPIDER_MINES,
item_names.REAPER_LASER_TARGETING_SYSTEM,
item_names.REAPER_PROGRESSIVE_STIMPACK,
item_names.REAPER_ADVANCED_CLOAKING_FIELD,
# Reaper special ordnance gives anti-building attack, which is baseline in AP
item_names.HELLION_JUMP_JETS,
item_names.HELLION_PROGRESSIVE_STIMPACK,
item_names.HELLION_SMART_SERVOS,
item_names.HELLION_OPTIMIZED_LOGISTICS,
item_names.HELLION_THERMITE_FILAMENTS, # Called Infernal Pre-Igniter in NCO
item_names.GOLIATH_ARES_CLASS_TARGETING_SYSTEM, # Called Laser Targeting System in NCO
item_names.GOLIATH_JUMP_JETS,
item_names.GOLIATH_OPTIMIZED_LOGISTICS,
item_names.GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM,
item_names.SIEGE_TANK_SPIDER_MINES,
item_names.SIEGE_TANK_JUMP_JETS,
item_names.SIEGE_TANK_INTERNAL_TECH_MODULE,
item_names.SIEGE_TANK_SMART_SERVOS,
# Tanks can't get Laser targeting system in NCO
item_names.BANSHEE_INTERNAL_TECH_MODULE,
item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS,
item_names.BANSHEE_SHOCKWAVE_MISSILE_BATTERY, # Banshee Special Ordnance
# Banshees can't get laser targeting systems in NCO
item_names.LIBERATOR_CLOAK,
item_names.LIBERATOR_SMART_SERVOS,
item_names.LIBERATOR_OPTIMIZED_LOGISTICS,
# Liberators can't get laser targeting system in NCO
item_names.RAVEN_SPIDER_MINES,
item_names.RAVEN_INTERNAL_TECH_MODULE,
item_names.RAVEN_RAILGUN_TURRET, # Raven Magrail Munitions
item_names.RAVEN_HUNTER_SEEKER_WEAPON, # Raven Special Ordnance
item_names.BATTLECRUISER_INTERNAL_TECH_MODULE,
item_names.BATTLECRUISER_CLOAK,
item_names.BATTLECRUISER_ATX_LASER_BATTERY, # Battlecruiser Special Ordnance
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL,
]
item_name_groups[ItemGroupNames.NCO_UPGRADES] = nco_upgrades = nco_baseline_upgrades + nco_unit_technology
item_name_groups[ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS] = nco_unit_technology + nova_equipment + terran_generic_upgrades
item_name_groups[ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS] = nco_units + nco_baseline_upgrades
item_name_groups[ItemGroupNames.TERRAN_PROGRESSIVE_UPGRADES] = terran_progressive_items = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type in (item_tables.TerranItemType.Progressive, item_tables.TerranItemType.Progressive_2)
]
item_name_groups[ItemGroupNames.WOL_ITEMS] = vanilla_wol_items = (
wol_units
+ wol_buildings
+ wol_mercs
+ wol_upgrades
+ orbital_command_abilities
+ terran_generic_upgrades
)
# Zerg
item_name_groups[ItemGroupNames.ZERG_ITEMS] = zerg_items = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.race == SC2Race.ZERG
]
item_name_groups[ItemGroupNames.ZERG_BUILDINGS] = zerg_buildings = [
item_names.SPINE_CRAWLER,
item_names.SPORE_CRAWLER,
item_names.BILE_LAUNCHER,
item_names.INFESTED_BUNKER,
item_names.INFESTED_MISSILE_TURRET,
item_names.NYDUS_WORM,
item_names.ECHIDNA_WORM]
item_name_groups[ItemGroupNames.ZERG_NONMORPH_UNITS] = zerg_nonmorph_units = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type in (
item_tables.ZergItemType.Unit, item_tables.ZergItemType.Mercenary
)
and item_name not in zerg_buildings
]
item_name_groups[ItemGroupNames.ZERG_MORPHS] = zerg_morphs = [
item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ZergItemType.Morph
]
item_name_groups[ItemGroupNames.ZERG_UNITS] = zerg_units = zerg_nonmorph_units + zerg_morphs
# For W/A upgrades
zerg_ground_units = [
item_names.ZERGLING, item_names.SWARM_QUEEN, item_names.ROACH, item_names.HYDRALISK, item_names.ABERRATION,
item_names.SWARM_HOST, item_names.INFESTOR, item_names.ULTRALISK, item_names.ZERGLING_BANELING_ASPECT,
item_names.HYDRALISK_LURKER_ASPECT, item_names.HYDRALISK_IMPALER_ASPECT, item_names.ULTRALISK_TYRANNOZOR_ASPECT,
item_names.ROACH_RAVAGER_ASPECT, item_names.DEFILER, item_names.ROACH_PRIMAL_IGNITER_ASPECT,
item_names.PYGALISK,
item_names.INFESTED_MARINE, item_names.INFESTED_BUNKER, item_names.INFESTED_DIAMONDBACK,
item_names.INFESTED_SIEGE_TANK,
]
zerg_melee_wa = [
item_names.ZERGLING, item_names.ABERRATION, item_names.ULTRALISK, item_names.ZERGLING_BANELING_ASPECT,
item_names.ULTRALISK_TYRANNOZOR_ASPECT, item_names.INFESTED_BUNKER, item_names.PYGALISK,
]
zerg_ranged_wa = [
item_names.SWARM_QUEEN, item_names.ROACH, item_names.HYDRALISK, item_names.SWARM_HOST,
item_names.HYDRALISK_LURKER_ASPECT, item_names.HYDRALISK_IMPALER_ASPECT, item_names.ULTRALISK_TYRANNOZOR_ASPECT,
item_names.ROACH_RAVAGER_ASPECT, item_names.ROACH_PRIMAL_IGNITER_ASPECT, item_names.INFESTED_MARINE,
item_names.INFESTED_BUNKER, item_names.INFESTED_DIAMONDBACK, item_names.INFESTED_SIEGE_TANK,
]
zerg_air_units = [
item_names.MUTALISK, item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT, item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT,
item_names.CORRUPTOR, item_names.BROOD_QUEEN, item_names.SCOURGE, item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT,
item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, item_names.INFESTED_BANSHEE, item_names.INFESTED_LIBERATOR,
]
item_name_groups[ItemGroupNames.ZERG_GENERIC_UPGRADES] = zerg_generic_upgrades = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type == item_tables.ZergItemType.Upgrade
]
item_name_groups[ItemGroupNames.HOTS_UNITS] = hots_units = [
item_names.ZERGLING, item_names.SWARM_QUEEN, item_names.ROACH, item_names.HYDRALISK,
item_names.ABERRATION, item_names.SWARM_HOST, item_names.MUTALISK,
item_names.INFESTOR, item_names.ULTRALISK,
item_names.ZERGLING_BANELING_ASPECT,
item_names.HYDRALISK_LURKER_ASPECT,
item_names.HYDRALISK_IMPALER_ASPECT,
item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT,
item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT,
]
item_name_groups[ItemGroupNames.HOTS_BUILDINGS] = hots_buildings = [
item_names.SPINE_CRAWLER,
item_names.SPORE_CRAWLER,
]
item_name_groups[ItemGroupNames.HOTS_MORPHS] = hots_morphs = [
item_names.ZERGLING_BANELING_ASPECT,
item_names.HYDRALISK_IMPALER_ASPECT,
item_names.HYDRALISK_LURKER_ASPECT,
item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT,
item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT,
]
item_name_groups[ItemGroupNames.ZERG_MERCENARIES] = zerg_mercenaries = [
item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ZergItemType.Mercenary
]
item_name_groups[ItemGroupNames.KERRIGAN_ABILITIES] = kerrigan_abilities = [
item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ZergItemType.Ability
]
item_name_groups[ItemGroupNames.KERRIGAN_PASSIVES] = kerrigan_passives = [
item_names.KERRIGAN_HEROIC_FORTITUDE, item_names.KERRIGAN_CHAIN_REACTION,
item_names.KERRIGAN_INFEST_BROODLINGS, item_names.KERRIGAN_FURY, item_names.KERRIGAN_ABILITY_EFFICIENCY,
]
item_name_groups[ItemGroupNames.KERRIGAN_ACTIVE_ABILITIES] = kerrigan_active_abilities = [
item_name for item_name in kerrigan_abilities if item_name not in kerrigan_passives
]
item_name_groups[ItemGroupNames.KERRIGAN_LOGIC_ACTIVE_ABILITIES] = kerrigan_logic_active_abilities = [
item_name for item_name in kerrigan_active_abilities if item_name != item_names.KERRIGAN_ASSIMILATION_AURA
]
item_name_groups[ItemGroupNames.KERRIGAN_TIER_1] = kerrigan_tier_1 = [
item_names.KERRIGAN_CRUSHING_GRIP, item_names.KERRIGAN_HEROIC_FORTITUDE, item_names.KERRIGAN_LEAPING_STRIKE
]
item_name_groups[ItemGroupNames.KERRIGAN_TIER_2] = kerrigan_tier_2= [
item_names.KERRIGAN_CRUSHING_GRIP, item_names.KERRIGAN_CHAIN_REACTION, item_names.KERRIGAN_PSIONIC_SHIFT
]
item_name_groups[ItemGroupNames.KERRIGAN_TIER_3] = kerrigan_tier_3 = [
item_names.TWIN_DRONES, item_names.AUTOMATED_EXTRACTORS, item_names.ZERGLING_RECONSTITUTION
]
item_name_groups[ItemGroupNames.KERRIGAN_TIER_4] = kerrigan_tier_4 = [
item_names.KERRIGAN_MEND, item_names.KERRIGAN_SPAWN_BANELINGS, item_names.KERRIGAN_WILD_MUTATION
]
item_name_groups[ItemGroupNames.KERRIGAN_TIER_5] = kerrigan_tier_5 = [
item_names.MALIGNANT_CREEP, item_names.VESPENE_EFFICIENCY, item_names.OVERLORD_IMPROVED_OVERLORDS
]
item_name_groups[ItemGroupNames.KERRIGAN_TIER_6] = kerrigan_tier_6 = [
item_names.KERRIGAN_INFEST_BROODLINGS, item_names.KERRIGAN_FURY, item_names.KERRIGAN_ABILITY_EFFICIENCY
]
item_name_groups[ItemGroupNames.KERRIGAN_TIER_7] = kerrigan_tier_7 = [
item_names.KERRIGAN_APOCALYPSE, item_names.KERRIGAN_SPAWN_LEVIATHAN, item_names.KERRIGAN_DROP_PODS
]
item_name_groups[ItemGroupNames.KERRIGAN_ULTIMATES] = kerrigan_ultimates = [
*kerrigan_tier_7, item_names.KERRIGAN_ASSIMILATION_AURA, item_names.KERRIGAN_IMMOBILIZATION_WAVE
]
item_name_groups[ItemGroupNames.KERRIGAN_NON_ULTIMATES] = kerrigan_non_ulimates = [
item for item in kerrigan_abilities if item not in kerrigan_ultimates
]
item_name_groups[ItemGroupNames.KERRIGAN_LOGIC_ULTIMATES] = kerrigan_logic_ultimates = [
item for item in kerrigan_ultimates if item != item_names.KERRIGAN_ASSIMILATION_AURA
]
item_name_groups[ItemGroupNames.KERRIGAN_NON_ULTIMATE_ACTIVE_ABILITIES] = kerrigan_non_ulimate_active_abilities = [
item for item in kerrigan_non_ulimates if item in kerrigan_active_abilities
]
item_name_groups[ItemGroupNames.KERRIGAN_HOTS_ABILITIES] = kerrigan_hots_abilities = [
ability for tiers in [
kerrigan_tier_1, kerrigan_tier_2, kerrigan_tier_4, kerrigan_tier_6, kerrigan_tier_7
] for ability in tiers
]
item_name_groups[ItemGroupNames.OVERLORD_UPGRADES] = [
item_names.OVERLORD_ANTENNAE,
item_names.OVERLORD_VENTRAL_SACS,
item_names.OVERLORD_GENERATE_CREEP,
item_names.OVERLORD_PNEUMATIZED_CARAPACE,
item_names.OVERLORD_IMPROVED_OVERLORDS,
item_names.OVERLORD_OVERSEER_ASPECT,
]
# Zerg Upgrades
item_name_groups[ItemGroupNames.HOTS_STRAINS] = hots_strains = [
item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ZergItemType.Strain
]
item_name_groups[ItemGroupNames.HOTS_MUTATIONS] = hots_mutations = [
item_names.ZERGLING_HARDENED_CARAPACE, item_names.ZERGLING_ADRENAL_OVERLOAD, item_names.ZERGLING_METABOLIC_BOOST,
item_names.BANELING_CORROSIVE_ACID, item_names.BANELING_RUPTURE, item_names.BANELING_REGENERATIVE_ACID,
item_names.ROACH_HYDRIODIC_BILE, item_names.ROACH_ADAPTIVE_PLATING, item_names.ROACH_TUNNELING_CLAWS,
item_names.HYDRALISK_FRENZY, item_names.HYDRALISK_ANCILLARY_CARAPACE, item_names.HYDRALISK_GROOVED_SPINES,
item_names.SWARM_HOST_BURROW, item_names.SWARM_HOST_RAPID_INCUBATION, item_names.SWARM_HOST_PRESSURIZED_GLANDS,
item_names.MUTALISK_VICIOUS_GLAIVE, item_names.MUTALISK_RAPID_REGENERATION, item_names.MUTALISK_SUNDERING_GLAIVE,
item_names.ULTRALISK_BURROW_CHARGE, item_names.ULTRALISK_TISSUE_ASSIMILATION, item_names.ULTRALISK_MONARCH_BLADES,
]
item_name_groups[ItemGroupNames.HOTS_GLOBAL_UPGRADES] = hots_global_upgrades = [
item_names.ZERGLING_RECONSTITUTION,
item_names.OVERLORD_IMPROVED_OVERLORDS,
item_names.AUTOMATED_EXTRACTORS,
item_names.TWIN_DRONES,
item_names.MALIGNANT_CREEP,
item_names.VESPENE_EFFICIENCY,
]
item_name_groups[ItemGroupNames.HOTS_ITEMS] = vanilla_hots_items = (
hots_units
+ hots_buildings
+ kerrigan_hots_abilities
+ hots_mutations
+ hots_strains
+ hots_global_upgrades
+ zerg_generic_upgrades
)
# Zerg - Infested Terran (Stukov Co-op)
item_name_groups[ItemGroupNames.INF_TERRAN_UNITS] = infterr_units = [
item_names.INFESTED_MARINE,
item_names.INFESTED_BUNKER,
item_names.BULLFROG,
item_names.INFESTED_DIAMONDBACK,
item_names.INFESTED_SIEGE_TANK,
item_names.INFESTED_LIBERATOR,
item_names.INFESTED_BANSHEE,
]
item_name_groups[ItemGroupNames.INF_TERRAN_UPGRADES] = infterr_upgrades = [
item_names.INFESTED_SCV_BUILD_CHARGES,
item_names.INFESTED_MARINE_PLAGUED_MUNITIONS,
item_names.INFESTED_MARINE_RETINAL_AUGMENTATION,
item_names.INFESTED_BUNKER_CALCIFIED_ARMOR,
item_names.INFESTED_BUNKER_REGENERATIVE_PLATING,
item_names.INFESTED_BUNKER_ENGORGED_BUNKERS,
item_names.BULLFROG_WILD_MUTATION,
item_names.BULLFROG_BROODLINGS,
item_names.BULLFROG_HARD_IMPACT,
item_names.BULLFROG_RANGE,
item_names.INFESTED_DIAMONDBACK_CAUSTIC_MUCUS,
item_names.INFESTED_DIAMONDBACK_CONCENTRATED_SPEW,
item_names.INFESTED_DIAMONDBACK_PROGRESSIVE_FUNGAL_SNARE,
item_names.INFESTED_DIAMONDBACK_VIOLENT_ENZYMES,
item_names.INFESTED_SIEGE_TANK_ACIDIC_ENZYMES,
item_names.INFESTED_SIEGE_TANK_BALANCED_ROOTS,
item_names.INFESTED_SIEGE_TANK_DEEP_TUNNEL,
item_names.INFESTED_SIEGE_TANK_PROGRESSIVE_AUTOMATED_MITOSIS,
item_names.INFESTED_SIEGE_TANK_SEISMIC_SONAR,
item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL,
item_names.INFESTED_LIBERATOR_DEFENDER_MODE,
item_names.INFESTED_LIBERATOR_VIRAL_CONTAMINATION,
item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS,
item_names.INFESTED_BANSHEE_BRACED_EXOSKELETON,
item_names.INFESTED_BANSHEE_RAPID_HIBERNATION,
item_names.INFESTED_DIAMONDBACK_FRIGHTFUL_FLESHWELDER,
item_names.INFESTED_SIEGE_TANK_FRIGHTFUL_FLESHWELDER,
item_names.INFESTED_LIBERATOR_FRIGHTFUL_FLESHWELDER,
item_names.INFESTED_BANSHEE_FRIGHTFUL_FLESHWELDER,
item_names.INFESTED_MISSILE_TURRET_BIOELECTRIC_PAYLOAD,
item_names.INFESTED_MISSILE_TURRET_ACID_SPORE_VENTS,
]
item_name_groups[ItemGroupNames.INF_TERRAN_ITEMS] = (
infterr_units
+ infterr_upgrades
+ [item_names.INFESTED_MISSILE_TURRET]
)
# Protoss
item_name_groups[ItemGroupNames.PROTOSS_ITEMS] = protoss_items = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.race == SC2Race.PROTOSS
]
item_name_groups[ItemGroupNames.PROTOSS_UNITS] = protoss_units = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type in (item_tables.ProtossItemType.Unit, item_tables.ProtossItemType.Unit_2)
]
protoss_ground_wa = [
item_names.ZEALOT, item_names.CENTURION, item_names.SENTINEL, item_names.SUPPLICANT,
item_names.SENTRY, item_names.ENERGIZER,
item_names.STALKER, item_names.INSTIGATOR, item_names.SLAYER, item_names.DRAGOON, item_names.ADEPT,
item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.ASCENDANT,
item_names.DARK_TEMPLAR, item_names.BLOOD_HUNTER, item_names.AVENGER,
item_names.DARK_ARCHON,
item_names.IMMORTAL, item_names.ANNIHILATOR, item_names.VANGUARD, item_names.STALWART,
item_names.COLOSSUS, item_names.WRATHWALKER,
item_names.REAVER,
]
protoss_air_wa = [
item_names.WARP_PRISM_PHASE_BLASTER,
item_names.PHOENIX, item_names.MIRAGE, item_names.CORSAIR, item_names.SKIRMISHER,
item_names.VOID_RAY, item_names.DESTROYER, item_names.PULSAR, item_names.DAWNBRINGER,
item_names.CARRIER, item_names.SKYLORD, item_names.TRIREME,
item_names.SCOUT, item_names.TEMPEST, item_names.MOTHERSHIP,
item_names.ARBITER, item_names.ORACLE, item_names.OPPRESSOR,
item_names.CALADRIUS, item_names.MISTWING,
]
item_name_groups[ItemGroupNames.PROTOSS_GENERIC_UPGRADES] = protoss_generic_upgrades = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type == item_tables.ProtossItemType.Upgrade
]
item_name_groups[ItemGroupNames.LOTV_UNITS] = lotv_units = [
item_names.ZEALOT, item_names.CENTURION, item_names.SENTINEL,
item_names.STALKER, item_names.DRAGOON, item_names.ADEPT,
item_names.SENTRY, item_names.HAVOC, item_names.ENERGIZER,
item_names.HIGH_TEMPLAR, item_names.DARK_ARCHON, item_names.ASCENDANT,
item_names.DARK_TEMPLAR, item_names.AVENGER, item_names.BLOOD_HUNTER,
item_names.IMMORTAL, item_names.ANNIHILATOR, item_names.VANGUARD,
item_names.COLOSSUS, item_names.WRATHWALKER, item_names.REAVER,
item_names.PHOENIX, item_names.MIRAGE, item_names.CORSAIR,
item_names.VOID_RAY, item_names.DESTROYER, item_names.ARBITER,
item_names.CARRIER, item_names.TEMPEST, item_names.MOTHERSHIP,
]
item_name_groups[ItemGroupNames.PROPHECY_UNITS] = prophecy_units = [
item_names.ZEALOT, item_names.STALKER, item_names.HIGH_TEMPLAR, item_names.DARK_TEMPLAR,
item_names.OBSERVER, item_names.COLOSSUS,
item_names.PHOENIX, item_names.VOID_RAY, item_names.CARRIER,
]
item_name_groups[ItemGroupNames.PROPHECY_BUILDINGS] = prophecy_buildings = [
item_names.PHOTON_CANNON,
]
item_name_groups[ItemGroupNames.GATEWAY_UNITS] = gateway_units = [
item_names.ZEALOT, item_names.CENTURION, item_names.SENTINEL, item_names.SUPPLICANT,
item_names.STALKER, item_names.INSTIGATOR, item_names.SLAYER,
item_names.SENTRY, item_names.HAVOC, item_names.ENERGIZER,
item_names.DRAGOON, item_names.ADEPT, item_names.DARK_ARCHON,
item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.ASCENDANT,
item_names.DARK_TEMPLAR, item_names.AVENGER, item_names.BLOOD_HUNTER,
]
item_name_groups[ItemGroupNames.ROBO_UNITS] = robo_units = [
item_names.WARP_PRISM, item_names.OBSERVER,
item_names.IMMORTAL, item_names.ANNIHILATOR, item_names.VANGUARD, item_names.STALWART,
item_names.COLOSSUS, item_names.WRATHWALKER,
item_names.REAVER, item_names.DISRUPTOR,
]
item_name_groups[ItemGroupNames.STARGATE_UNITS] = stargate_units = [
item_names.PHOENIX, item_names.SKIRMISHER, item_names.MIRAGE, item_names.CORSAIR,
item_names.VOID_RAY, item_names.DESTROYER, item_names.PULSAR, item_names.DAWNBRINGER,
item_names.CARRIER, item_names.SKYLORD, item_names.TRIREME,
item_names.TEMPEST, item_names.SCOUT, item_names.MOTHERSHIP,
item_names.ARBITER, item_names.ORACLE, item_names.OPPRESSOR,
item_names.CALADRIUS, item_names.MISTWING,
]
item_name_groups[ItemGroupNames.PROTOSS_BUILDINGS] = protoss_buildings = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type == item_tables.ProtossItemType.Building
]
item_name_groups[ItemGroupNames.AIUR_UNITS] = [
item_names.ZEALOT, item_names.DRAGOON, item_names.SENTRY, item_names.AVENGER, item_names.HIGH_TEMPLAR,
item_names.IMMORTAL, item_names.REAVER,
item_names.PHOENIX, item_names.SCOUT, item_names.ARBITER, item_names.CARRIER,
]
item_name_groups[ItemGroupNames.NERAZIM_UNITS] = [
item_names.CENTURION, item_names.STALKER, item_names.DARK_TEMPLAR, item_names.SIGNIFIER, item_names.DARK_ARCHON,
item_names.ANNIHILATOR,
item_names.CORSAIR, item_names.ORACLE, item_names.VOID_RAY, item_names.MISTWING,
]
item_name_groups[ItemGroupNames.TAL_DARIM_UNITS] = [
item_names.SUPPLICANT, item_names.SLAYER, item_names.HAVOC, item_names.BLOOD_HUNTER, item_names.ASCENDANT,
item_names.VANGUARD, item_names.WRATHWALKER,
item_names.SKIRMISHER, item_names.DESTROYER, item_names.SKYLORD, item_names.MOTHERSHIP, item_names.OPPRESSOR,
]
item_name_groups[ItemGroupNames.PURIFIER_UNITS] = [
item_names.SENTINEL, item_names.ADEPT, item_names.INSTIGATOR, item_names.ENERGIZER,
item_names.STALWART, item_names.COLOSSUS, item_names.DISRUPTOR,
item_names.MIRAGE, item_names.DAWNBRINGER, item_names.TRIREME, item_names.TEMPEST,
item_names.CALADRIUS,
]
item_name_groups[ItemGroupNames.SOA_ITEMS] = soa_items = [
*[item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Spear_Of_Adun],
item_names.SOA_PROGRESSIVE_PROXY_PYLON,
]
lotv_soa_items = [item_name for item_name in soa_items if item_name != item_names.SOA_PYLON_OVERCHARGE]
item_name_groups[ItemGroupNames.PROTOSS_GLOBAL_UPGRADES] = [
item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Solarite_Core
]
item_name_groups[ItemGroupNames.LOTV_GLOBAL_UPGRADES] = lotv_global_upgrades = [
item_names.NEXUS_OVERCHARGE,
item_names.ORBITAL_ASSIMILATORS,
item_names.WARP_HARMONIZATION,
item_names.MATRIX_OVERLOAD,
item_names.GUARDIAN_SHELL,
item_names.RECONSTRUCTION_BEAM,
]
item_name_groups[ItemGroupNames.WAR_COUNCIL] = war_council_upgrades = [
item_name for item_name, item_data in item_tables.item_table.items()
if item_data.type in (item_tables.ProtossItemType.War_Council, item_tables.ProtossItemType.War_Council_2)
]
lotv_war_council_upgrades = [
item_name for item_name, item_data in item_tables.item_table.items()
if (
item_name in war_council_upgrades
and item_data.parent in item_name_groups[ItemGroupNames.LOTV_UNITS]
# Destroyers get a custom (non-vanilla) buff, not a nerf over their vanilla council state
and item_name != item_names.DESTROYER_REFORGED_BLOODSHARD_CORE
)
]
item_name_groups[ItemGroupNames.LOTV_ITEMS] = vanilla_lotv_items = (
lotv_units
+ protoss_buildings
+ lotv_soa_items
+ lotv_global_upgrades
+ protoss_generic_upgrades
+ lotv_war_council_upgrades
)
item_name_groups[ItemGroupNames.VANILLA_ITEMS] = vanilla_items = (
vanilla_wol_items + vanilla_hots_items + vanilla_lotv_items
)
item_name_groups[ItemGroupNames.OVERPOWERED_ITEMS] = overpowered_items = [
# Terran general
item_names.SIEGE_TANK_GRADUATING_RANGE,
item_names.RAVEN_HUNTER_SEEKER_WEAPON,
item_names.BATTLECRUISER_ATX_LASER_BATTERY,
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL,
item_names.MECHANICAL_KNOW_HOW,
item_names.MERCENARY_MUNITIONS,
# Terran Mind Control
item_names.HIVE_MIND_EMULATOR,
item_names.PSI_INDOCTRINATOR,
item_names.ARGUS_AMPLIFIER,
# Zerg Mind Control
item_names.INFESTOR,
# Protoss Mind Control
item_names.DARK_ARCHON_INDOMITABLE_WILL,
# Nova
item_names.NOVA_PLASMA_RIFLE,
# Kerrigan
item_names.KERRIGAN_APOCALYPSE,
item_names.KERRIGAN_DROP_PODS,
item_names.KERRIGAN_SPAWN_LEVIATHAN,
item_names.KERRIGAN_IMMOBILIZATION_WAVE,
# SOA
item_names.SOA_TIME_STOP,
item_names.SOA_SOLAR_LANCE,
item_names.SOA_DEPLOY_FENIX,
# Note: This is more an issue of having multiple ults at the same time, rather than solar bombardment in particular.
# Can be removed from the list if we get an SOA ult combined cooldown or energy cost on it.
item_names.SOA_SOLAR_BOMBARDMENT,
# Protoss general
item_names.QUATRO,
item_names.MOTHERSHIP_INTEGRATED_POWER,
item_names.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING,
# Mindless Broodwar garbage
item_names.GHOST_BARGAIN_BIN_PRICES,
item_names.SPECTRE_BARGAIN_BIN_PRICES,
item_names.REAVER_BARGAIN_BIN_PRICES,
item_names.SCOUT_SUPPLY_EFFICIENCY,
]
# Items not aimed to be officially released
# These need further balancing, and they shouldn't generate normally unless explicitly locked
# Added here to not confuse the client
item_name_groups[ItemGroupNames.UNRELEASED_ITEMS] = unreleased_items = [
item_names.PRIDE_OF_AUGUSTRGRAD,
item_names.SKY_FURY,
item_names.SHOCK_DIVISION,
item_names.BLACKHAMMER,
item_names.AEGIS_GUARD,
item_names.EMPERORS_SHADOW,
item_names.SON_OF_KORHAL,
item_names.BULWARK_COMPANY,
item_names.FIELD_RESPONSE_THETA,
item_names.EMPERORS_GUARDIAN,
item_names.NIGHT_HAWK,
item_names.NIGHT_WOLF,
item_names.EMPERORS_SHADOW_SOVEREIGN_TACTICAL_MISSILES,
]
# A place for traits that were released before but are to be taken down by default.
# If an item gets split to multiple ones, the original one should be set deprecated instead (see Orbital Command for an example).
# This is a place if you want to nerf or disable by default a previously released trait.
# Currently, it disables only the topmost level of the progressives.
# Don't place here anything that's present in the vanilla campaigns (if it's overpowered, use overpowered items instead)
item_name_groups[ItemGroupNames.LEGACY_ITEMS] = legacy_items = [
item_names.ASCENDANT_ARCHON_MERGE,
]
item_name_groups[ItemGroupNames.KEYS] = keys = [
item_name for item_name in key_item_table.keys()
]

View File

@@ -0,0 +1,957 @@
"""
A complete collection of Starcraft 2 item names as strings.
Users of this data may make some assumptions about the structure of a name:
* The upgrade for a unit will end with the unit's name in parentheses
* Weapon / armor upgrades may be grouped by a common prefix specified within this file
"""
# Terran Units
MARINE = "Marine"
MEDIC = "Medic"
FIREBAT = "Firebat"
MARAUDER = "Marauder"
REAPER = "Reaper"
HELLION = "Hellion"
VULTURE = "Vulture"
GOLIATH = "Goliath"
DIAMONDBACK = "Diamondback"
SIEGE_TANK = "Siege Tank"
MEDIVAC = "Medivac"
WRAITH = "Wraith"
VIKING = "Viking"
BANSHEE = "Banshee"
BATTLECRUISER = "Battlecruiser"
GHOST = "Ghost"
SPECTRE = "Spectre"
THOR = "Thor"
RAVEN = "Raven"
SCIENCE_VESSEL = "Science Vessel"
PREDATOR = "Predator"
HERCULES = "Hercules"
# Extended units
LIBERATOR = "Liberator"
VALKYRIE = "Valkyrie"
WIDOW_MINE = "Widow Mine"
CYCLONE = "Cyclone"
HERC = "HERC"
WARHOUND = "Warhound"
DOMINION_TROOPER = "Dominion Trooper"
# Elites
PRIDE_OF_AUGUSTRGRAD = "Pride of Augustgrad"
SKY_FURY = "Sky Fury"
SHOCK_DIVISION = "Shock Division"
BLACKHAMMER = "Blackhammer"
AEGIS_GUARD = "Aegis Guard"
EMPERORS_SHADOW = "Emperor's Shadow"
SON_OF_KORHAL = "Son of Korhal"
BULWARK_COMPANY = "Bulwark Company"
FIELD_RESPONSE_THETA = "Field Response Theta"
EMPERORS_GUARDIAN = "Emperor's Guardian"
NIGHT_HAWK = "Night Hawk"
NIGHT_WOLF = "Night Wolf"
# Terran Buildings
BUNKER = "Bunker"
MISSILE_TURRET = "Missile Turret"
SENSOR_TOWER = "Sensor Tower"
PLANETARY_FORTRESS = "Planetary Fortress"
PERDITION_TURRET = "Perdition Turret"
# HIVE_MIND_EMULATOR = "Hive Mind Emulator"# moved to Lab / Global upgrades
# PSI_DISRUPTER = "Psi Disrupter" # moved to Lab / Global upgrades
DEVASTATOR_TURRET = "Devastator Turret"
# Terran Weapon / Armor Upgrades
TERRAN_UPGRADE_PREFIX = "Progressive Terran"
TERRAN_INFANTRY_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Infantry"
TERRAN_VEHICLE_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Vehicle"
TERRAN_SHIP_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Ship"
PROGRESSIVE_TERRAN_INFANTRY_WEAPON = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_TERRAN_INFANTRY_ARMOR = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Armor"
PROGRESSIVE_TERRAN_VEHICLE_WEAPON = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_TERRAN_VEHICLE_ARMOR = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Armor"
PROGRESSIVE_TERRAN_SHIP_WEAPON = f"{TERRAN_SHIP_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_TERRAN_SHIP_ARMOR = f"{TERRAN_SHIP_UPGRADE_PREFIX} Armor"
PROGRESSIVE_TERRAN_WEAPON_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Weapon Upgrade"
PROGRESSIVE_TERRAN_ARMOR_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Armor Upgrade"
PROGRESSIVE_TERRAN_INFANTRY_UPGRADE = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_TERRAN_VEHICLE_UPGRADE = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_TERRAN_SHIP_UPGRADE = f"{TERRAN_SHIP_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Weapon/Armor Upgrade"
# Mercenaries
WAR_PIGS = "War Pigs"
DEVIL_DOGS = "Devil Dogs"
HAMMER_SECURITIES = "Hammer Securities"
SPARTAN_COMPANY = "Spartan Company"
SIEGE_BREAKERS = "Siege Breakers"
HELS_ANGELS = "Hel's Angels"
DUSK_WINGS = "Dusk Wings"
JACKSONS_REVENGE = "Jackson's Revenge"
SKIBIS_ANGELS = "Skibi's Angels"
DEATH_HEADS = "Death Heads"
WINGED_NIGHTMARES = "Winged Nightmares"
MIDNIGHT_RIDERS = "Midnight Riders"
BRYNHILDS = "Brynhilds"
JOTUN = "Jotun"
# Lab / Global
ULTRA_CAPACITORS = "Ultra-Capacitors (Terran)"
VANADIUM_PLATING = "Vanadium Plating (Terran)"
ORBITAL_DEPOTS = "Orbital Depots (Terran)"
MICRO_FILTERING = "Micro-Filtering (Terran)"
AUTOMATED_REFINERY = "Automated Refinery (Terran)"
COMMAND_CENTER_COMMAND_CENTER_REACTOR = "Command Center Reactor (Command Center)"
COMMAND_CENTER_SCANNER_SWEEP = "Scanner Sweep (Command Center)"
COMMAND_CENTER_MULE = "MULE (Command Center)"
COMMAND_CENTER_EXTRA_SUPPLIES = "Extra Supplies (Command Center)"
TECH_REACTOR = "Tech Reactor (Terran)"
ORBITAL_STRIKE = "Orbital Strike (Barracks)"
CELLULAR_REACTOR = "Cellular Reactor (Terran)"
PROGRESSIVE_REGENERATIVE_BIO_STEEL = "Progressive Regenerative Bio-Steel (Terran)"
PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM = "Progressive Fire-Suppression System (Terran)"
STRUCTURE_ARMOR = "Structure Armor (Terran)"
HI_SEC_AUTO_TRACKING = "Hi-Sec Auto Tracking (Terran)"
ADVANCED_OPTICS = "Advanced Optics (Terran)"
ROGUE_FORCES = "Rogue Forces (Terran)"
MECHANICAL_KNOW_HOW = "Mechanical Know-how (Terran)"
MERCENARY_MUNITIONS = "Mercenary Munitions (Terran)"
PROGRESSIVE_FAST_DELIVERY = "Progressive Fast Delivery (Terran)"
RAPID_REINFORCEMENT = "Rapid Reinforcement (Terran)"
FUSION_CORE_FUSION_REACTOR = "Fusion Reactor (Fusion Core)"
PSI_DISRUPTER = "Psi Disrupter"
PSI_SCREEN = "Psi Screen (Psi Disrupter)"
SONIC_DISRUPTER = "Sonic Disrupter (Psi Disrupter)"
HIVE_MIND_EMULATOR = "Hive Mind Emulator"
PSI_INDOCTRINATOR = "Psi Indoctrinator (Hive Mind Emulator)"
ARGUS_AMPLIFIER = "Argus Amplifier (Hive Mind Emulator)"
SIGNAL_BEACON = "Signal Beacon (Terran)"
# Terran Unit Upgrades
BANSHEE_HYPERFLIGHT_ROTORS = "Hyperflight Rotors (Banshee)"
BANSHEE_INTERNAL_TECH_MODULE = "Internal Tech Module (Banshee)"
BANSHEE_LASER_TARGETING_SYSTEM = "Laser Targeting System (Banshee)"
BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS = "Progressive Cross-Spectrum Dampeners (Banshee)"
BANSHEE_SHOCKWAVE_MISSILE_BATTERY = "Shockwave Missile Battery (Banshee)"
BANSHEE_SHAPED_HULL = "Shaped Hull (Banshee)"
BANSHEE_ADVANCED_TARGETING_OPTICS = "Advanced Targeting Optics (Banshee)"
BANSHEE_DISTORTION_BLASTERS = "Distortion Blasters (Banshee)"
BANSHEE_ROCKET_BARRAGE = "Rocket Barrage (Banshee)"
BATTLECRUISER_ATX_LASER_BATTERY = "ATX Laser Battery (Battlecruiser)"
BATTLECRUISER_CLOAK = "Cloak (Battlecruiser)"
BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX = "Progressive Defensive Matrix (Battlecruiser)"
BATTLECRUISER_INTERNAL_TECH_MODULE = "Internal Tech Module (Battlecruiser)"
BATTLECRUISER_PROGRESSIVE_MISSILE_PODS = "Progressive Missile Pods (Battlecruiser)"
BATTLECRUISER_OPTIMIZED_LOGISTICS = "Optimized Logistics (Battlecruiser)"
BATTLECRUISER_TACTICAL_JUMP = "Tactical Jump (Battlecruiser)"
BATTLECRUISER_BEHEMOTH_PLATING = "Behemoth Plating (Battlecruiser)"
BATTLECRUISER_MOIRAI_IMPULSE_DRIVE = "Moirai Impulse Drive (Battlecruiser)"
BATTLECRUISER_BEHEMOTH_REACTOR = "Behemoth Reactor (Battlecruiser)"
BATTLECRUISER_FIELD_ASSIST_TARGETING_SYSTEM = "Field-Assist Target System (Battlecruiser)"
CYCLONE_MAG_FIELD_ACCELERATORS = "Mag-Field Accelerators (Cyclone)"
CYCLONE_MAG_FIELD_LAUNCHERS = "Mag-Field Launchers (Cyclone)"
CYCLONE_RAPID_FIRE_LAUNCHERS = "Rapid Fire Launchers (Cyclone)"
CYCLONE_TARGETING_OPTICS = "Targeting Optics (Cyclone)"
CYCLONE_RESOURCE_EFFICIENCY = "Resource Efficiency (Cyclone)"
CYCLONE_INTERNAL_TECH_MODULE = "Internal Tech Module (Cyclone)"
DIAMONDBACK_BURST_CAPACITORS = "Burst Capacitors (Diamondback)"
DIAMONDBACK_HYPERFLUXOR = "Hyperfluxor (Diamondback)"
DIAMONDBACK_RESOURCE_EFFICIENCY = "Resource Efficiency (Diamondback)"
DIAMONDBACK_SHAPED_HULL = "Shaped Hull (Diamondback)"
DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL = "Progressive Tri-Lithium Power Cell (Diamondback)"
DIAMONDBACK_MAGLEV_PROPULSION = "Maglev Propulsion (Diamondback)"
DOMINION_TROOPER_B2_HIGH_CAL_LMG = "B-2 High-Cal LMG (Dominion Trooper)"
DOMINION_TROOPER_CPO7_SALAMANDER_FLAMETHROWER = "CPO-7 Salamander Flamethrower (Dominion Trooper)"
DOMINION_TROOPER_HAILSTORM_LAUNCHER = "Hailstorm Launcher (Dominion Trooper)"
DOMINION_TROOPER_ADVANCED_ALLOYS = "Advanced Alloys (Dominion Trooper)"
DOMINION_TROOPER_OPTIMIZED_LOGISTICS = "Optimized Logistics (Dominion Trooper)"
EMPERORS_SHADOW_SOVEREIGN_TACTICAL_MISSILES = "Sovereign Tactical Missiles (Emperor's Shadow)"
FIREBAT_INCINERATOR_GAUNTLETS = "Incinerator Gauntlets (Firebat)"
FIREBAT_JUGGERNAUT_PLATING = "Juggernaut Plating (Firebat)"
FIREBAT_RESOURCE_EFFICIENCY = "Resource Efficiency (Firebat)"
FIREBAT_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Firebat)"
FIREBAT_INFERNAL_PRE_IGNITER = "Infernal Pre-Igniter (Firebat)"
FIREBAT_KINETIC_FOAM = "Kinetic Foam (Firebat)"
FIREBAT_NANO_PROJECTORS = "Nano Projectors (Firebat)"
GHOST_CRIUS_SUIT = "Crius Suit (Ghost)"
GHOST_EMP_ROUNDS = "EMP Rounds (Ghost)"
GHOST_LOCKDOWN = "Lockdown (Ghost)"
GHOST_OCULAR_IMPLANTS = "Ocular Implants (Ghost)"
GHOST_RESOURCE_EFFICIENCY = "Resource Efficiency (Ghost)"
GHOST_BARGAIN_BIN_PRICES = "Bargain Bin Prices (Ghost)"
GOLIATH_ARES_CLASS_TARGETING_SYSTEM = "Ares-Class Targeting System (Goliath)"
GOLIATH_JUMP_JETS = "Jump Jets (Goliath)"
GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM = "Multi-Lock Weapons System (Goliath)"
GOLIATH_OPTIMIZED_LOGISTICS = "Optimized Logistics (Goliath)"
GOLIATH_SHAPED_HULL = "Shaped Hull (Goliath)"
GOLIATH_RESOURCE_EFFICIENCY = "Resource Efficiency (Goliath)"
GOLIATH_INTERNAL_TECH_MODULE = "Internal Tech Module (Goliath)"
HELLION_HELLBAT = "Hellbat (Hellion Morph)"
HELLION_JUMP_JETS = "Jump Jets (Hellion)"
HELLION_OPTIMIZED_LOGISTICS = "Optimized Logistics (Hellion)"
HELLION_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Hellion)"
HELLION_SMART_SERVOS = "Smart Servos (Hellion)"
HELLION_THERMITE_FILAMENTS = "Thermite Filaments (Hellion)"
HELLION_TWIN_LINKED_FLAMETHROWER = "Twin-Linked Flamethrower (Hellion)"
HELLION_INFERNAL_PLATING = "Infernal Plating (Hellion)"
HERC_JUGGERNAUT_PLATING = "Juggernaut Plating (HERC)"
HERC_KINETIC_FOAM = "Kinetic Foam (HERC)"
HERC_RESOURCE_EFFICIENCY = "Resource Efficiency (HERC)"
HERC_GRAPPLE_PULL = "Grapple Pull (HERC)"
HERCULES_INTERNAL_FUSION_MODULE = "Internal Fusion Module (Hercules)"
HERCULES_TACTICAL_JUMP = "Tactical Jump (Hercules)"
LIBERATOR_ADVANCED_BALLISTICS = "Advanced Ballistics (Liberator)"
LIBERATOR_CLOAK = "Cloak (Liberator)"
LIBERATOR_LASER_TARGETING_SYSTEM = "Laser Targeting System (Liberator)"
LIBERATOR_OPTIMIZED_LOGISTICS = "Optimized Logistics (Liberator)"
LIBERATOR_RAID_ARTILLERY = "Raid Artillery (Liberator)"
LIBERATOR_SMART_SERVOS = "Smart Servos (Liberator)"
LIBERATOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Liberator)"
LIBERATOR_GUERILLA_MISSILES = "Guerilla Missiles (Liberator)"
LIBERATOR_UED_MISSILE_TECHNOLOGY = "UED Missile Technology (Liberator)"
MARAUDER_CONCUSSIVE_SHELLS = "Concussive Shells (Marauder)"
MARAUDER_INTERNAL_TECH_MODULE = "Internal Tech Module (Marauder)"
MARAUDER_KINETIC_FOAM = "Kinetic Foam (Marauder)"
MARAUDER_LASER_TARGETING_SYSTEM = "Laser Targeting System (Marauder)"
MARAUDER_MAGRAIL_MUNITIONS = "Magrail Munitions (Marauder)"
MARAUDER_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Marauder)"
MARAUDER_JUGGERNAUT_PLATING = "Juggernaut Plating (Marauder)"
MARINE_COMBAT_SHIELD = "Combat Shield (Marine)"
MARINE_LASER_TARGETING_SYSTEM = "Laser Targeting System (Marine)"
MARINE_MAGRAIL_MUNITIONS = "Magrail Munitions (Marine)"
MARINE_OPTIMIZED_LOGISTICS = "Optimized Logistics (Marine)"
MARINE_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Marine)"
MEDIC_ADVANCED_MEDIC_FACILITIES = "Advanced Medic Facilities (Medic)"
MEDIC_OPTICAL_FLARE = "Optical Flare (Medic)"
MEDIC_RESOURCE_EFFICIENCY = "Resource Efficiency (Medic)"
MEDIC_RESTORATION = "Restoration (Medic)"
MEDIC_STABILIZER_MEDPACKS = "Stabilizer Medpacks (Medic)"
MEDIC_ADAPTIVE_MEDPACKS = "Adaptive Medpacks (Medic)"
MEDIC_NANO_PROJECTOR = "Nano Projector (Medic)"
MEDIVAC_ADVANCED_HEALING_AI = "Advanced Healing AI (Medivac)"
MEDIVAC_AFTERBURNERS = "Afterburners (Medivac)"
MEDIVAC_EXPANDED_HULL = "Expanded Hull (Medivac)"
MEDIVAC_RAPID_DEPLOYMENT_TUBE = "Rapid Deployment Tube (Medivac)"
MEDIVAC_SCATTER_VEIL = "Scatter Veil (Medivac)"
MEDIVAC_ADVANCED_CLOAKING_FIELD = "Advanced Cloaking Field (Medivac)"
MEDIVAC_RAPID_REIGNITION_SYSTEMS = "Rapid Reignition Systems (Medivac)"
MEDIVAC_RESOURCE_EFFICIENCY = "Resource Efficiency (Medivac)"
PREDATOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Predator)"
PREDATOR_CLOAK = "Phase Cloak (Predator)"
PREDATOR_CHARGE = "Concussive Charge (Predator)"
PREDATOR_VESPENE_SYNTHESIS = "Vespene Synthesis (Predator)"
PREDATOR_ADAPTIVE_DEFENSES = "Adaptive Defenses (Predator)"
RAVEN_ANTI_ARMOR_MISSILE = "Anti-Armor Missile (Raven)"
RAVEN_BIO_MECHANICAL_REPAIR_DRONE = "Bio Mechanical Repair Drone (Raven)"
RAVEN_HUNTER_SEEKER_WEAPON = "Hunter-Seeker Weapon (Raven)"
RAVEN_INTERFERENCE_MATRIX = "Interference Matrix (Raven)"
RAVEN_INTERNAL_TECH_MODULE = "Internal Tech Module (Raven)"
RAVEN_RAILGUN_TURRET = "Railgun Turret (Raven)"
RAVEN_SPIDER_MINES = "Spider Mines (Raven)"
RAVEN_RESOURCE_EFFICIENCY = "Resource Efficiency (Raven)"
RAVEN_DURABLE_MATERIALS = "Durable Materials (Raven)"
REAPER_ADVANCED_CLOAKING_FIELD = "Advanced Cloaking Field (Reaper)"
REAPER_COMBAT_DRUGS = "Combat Drugs (Reaper)"
REAPER_G4_CLUSTERBOMB = "G-4 Clusterbomb (Reaper)"
REAPER_LASER_TARGETING_SYSTEM = "Laser Targeting System (Reaper)"
REAPER_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Reaper)"
REAPER_SPIDER_MINES = "Spider Mines (Reaper)"
REAPER_U238_ROUNDS = "U-238 Rounds (Reaper)"
REAPER_JET_PACK_OVERDRIVE = "Jet Pack Overdrive (Reaper)"
REAPER_RESOURCE_EFFICIENCY = "Resource Efficiency (Reaper)"
REAPER_BALLISTIC_FLIGHTSUIT = "Ballistic Flightsuit (Reaper)"
SCIENCE_VESSEL_DEFENSIVE_MATRIX = "Defensive Matrix (Science Vessel)"
SCIENCE_VESSEL_EMP_SHOCKWAVE = "EMP Shockwave (Science Vessel)"
SCIENCE_VESSEL_IMPROVED_NANO_REPAIR = "Improved Nano-Repair (Science Vessel)"
SCIENCE_VESSEL_MAGELLAN_COMPUTATION_SYSTEMS = "Magellan Computation Systems (Science Vessel)"
SCIENCE_VESSEL_TACTICAL_JUMP = "Tactical Jump (Science Vessel)"
SCV_ADVANCED_CONSTRUCTION = "Advanced Construction (SCV)"
SCV_DUAL_FUSION_WELDERS = "Dual-Fusion Welders (SCV)"
SCV_HOSTILE_ENVIRONMENT_ADAPTATION = "Hostile Environment Adaptation (SCV)"
SCV_CONSTRUCTION_JUMP_JETS = "Construction Jump Jets (SCV)"
SIEGE_TANK_ADVANCED_SIEGE_TECH = "Advanced Siege Tech (Siege Tank)"
SIEGE_TANK_GRADUATING_RANGE = "Graduating Range (Siege Tank)"
SIEGE_TANK_INTERNAL_TECH_MODULE = "Internal Tech Module (Siege Tank)"
SIEGE_TANK_JUMP_JETS = "Jump Jets (Siege Tank)"
SIEGE_TANK_LASER_TARGETING_SYSTEM = "Laser Targeting System (Siege Tank)"
SIEGE_TANK_MAELSTROM_ROUNDS = "Maelstrom Rounds (Siege Tank)"
SIEGE_TANK_SHAPED_BLAST = "Shaped Blast (Siege Tank)"
SIEGE_TANK_SMART_SERVOS = "Smart Servos (Siege Tank)"
SIEGE_TANK_SPIDER_MINES = "Spider Mines (Siege Tank)"
SIEGE_TANK_SHAPED_HULL = "Shaped Hull (Siege Tank)"
SIEGE_TANK_RESOURCE_EFFICIENCY = "Resource Efficiency (Siege Tank)"
SIEGE_TANK_PROGRESSIVE_TRANSPORT_HOOK = "Progressive Transport Hook (Siege Tank)"
SIEGE_TANK_ALLTERRAIN_TREADS = "All-Terrain Treads (Siege Tank)"
SPECTRE_IMPALER_ROUNDS = "Impaler Rounds (Spectre)"
SPECTRE_NYX_CLASS_CLOAKING_MODULE = "Nyx-Class Cloaking Module (Spectre)"
SPECTRE_PSIONIC_LASH = "Psionic Lash (Spectre)"
SPECTRE_RESOURCE_EFFICIENCY = "Resource Efficiency (Spectre)"
SPECTRE_BARGAIN_BIN_PRICES = "Bargain Bin Prices (Spectre)"
SPIDER_MINE_CERBERUS_MINE = "Cerberus Mine (Spider Mine)"
SPIDER_MINE_HIGH_EXPLOSIVE_MUNITION = "High Explosive Munition (Spider Mine)"
THOR_330MM_BARRAGE_CANNON = "330mm Barrage Cannon (Thor)"
THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL = "Progressive Immortality Protocol (Thor)"
THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD = "Progressive High Impact Payload (Thor)"
THOR_BUTTON_WITH_A_SKULL_ON_IT = "Button With a Skull on It (Thor)"
THOR_LASER_TARGETING_SYSTEM = "Laser Targeting System (Thor)"
THOR_LARGE_SCALE_FIELD_CONSTRUCTION = "Large Scale Field Construction (Thor)"
THOR_RAPID_RELOAD = "Rapid Reload (Thor)"
VALKYRIE_AFTERBURNERS = "Afterburners (Valkyrie)"
VALKYRIE_FLECHETTE_MISSILES = "Flechette Missiles (Valkyrie)"
VALKYRIE_ENHANCED_CLUSTER_LAUNCHERS = "Enhanced Cluster Launchers (Valkyrie)"
VALKYRIE_SHAPED_HULL = "Shaped Hull (Valkyrie)"
VALKYRIE_LAUNCHING_VECTOR_COMPENSATOR = "Launching Vector Compensator (Valkyrie)"
VALKYRIE_RESOURCE_EFFICIENCY = "Resource Efficiency (Valkyrie)"
VIKING_ANTI_MECHANICAL_MUNITION = "Anti-Mechanical Munition (Viking)"
VIKING_PHOBOS_CLASS_WEAPONS_SYSTEM = "Phobos-Class Weapons System (Viking)"
VIKING_RIPWAVE_MISSILES = "Ripwave Missiles (Viking)"
VIKING_SMART_SERVOS = "Smart Servos (Viking)"
VIKING_SHREDDER_ROUNDS = "Shredder Rounds (Viking)"
VIKING_WILD_MISSILES = "W.I.L.D. Missiles (Viking)"
VIKING_AESIR_TURBINES = "Aesir Turbines (Viking)"
VULTURE_AUTO_LAUNCHERS = "Auto Launchers (Vulture)"
VULTURE_ION_THRUSTERS = "Ion Thrusters (Vulture)"
VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE = "Progressive Replenishable Magazine (Vulture)"
VULTURE_JERRYRIGGED_PATCHUP = "Jerry-Rigged Patchup (Vulture)"
WARHOUND_RESOURCE_EFFICIENCY = "Resource Efficiency (Warhound)"
WARHOUND_AXIOM_PLATING = "Axiom Plating (Warhound)"
WARHOUND_DEPLOY_TURRET = "Deploy Turret (Warhound)"
WIDOW_MINE_BLACK_MARKET_LAUNCHERS = "Black Market Launchers (Widow Mine)"
WIDOW_MINE_CONCEALMENT = "Concealment (Widow Mine)"
WIDOW_MINE_DEMOLITION_PAYLOAD = "Demolition Payload (Widow Mine)"
WIDOW_MINE_DRILLING_CLAWS = "Drilling Claws (Widow Mine)"
WIDOW_MINE_EXECUTIONER_MISSILES = "Executioner Missiles (Widow Mine)"
WIDOW_MINE_RESOURCE_EFFICIENCY = "Resource Efficiency (Widow Mine)"
WRAITH_ADVANCED_LASER_TECHNOLOGY = "Advanced Laser Technology (Wraith)"
WRAITH_DISPLACEMENT_FIELD = "Displacement Field (Wraith)"
WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS = "Progressive Tomahawk Power Cells (Wraith)"
WRAITH_TRIGGER_OVERRIDE = "Trigger Override (Wraith)"
WRAITH_INTERNAL_TECH_MODULE = "Internal Tech Module (Wraith)"
WRAITH_RESOURCE_EFFICIENCY = "Resource Efficiency (Wraith)"
# Terran Building upgrades
BUNKER_NEOSTEEL_BUNKER = "Neosteel Bunker (Bunker)"
BUNKER_PROJECTILE_ACCELERATOR = "Projectile Accelerator (Bunker)"
BUNKER_SHRIKE_TURRET = "Shrike Turret (Bunker)"
BUNKER_FORTIFIED_BUNKER = "Fortified Bunker (Bunker)"
DEVASTATOR_TURRET_ANTI_ARMOR_MUNITIONS = "Anti-Armor Munitions (Devastator Turret)"
DEVASTATOR_TURRET_CONCUSSIVE_GRENADES = "Concussive Grenades (Devastator Turret)"
DEVASTATOR_TURRET_RESOURCE_EFFICIENCY = "Resource Efficiency (Devastator Turret)"
MISSILE_TURRET_HELLSTORM_BATTERIES = "Hellstorm Batteries (Missile Turret)"
MISSILE_TURRET_TITANIUM_HOUSING = "Titanium Housing (Missile Turret)"
MISSILE_TURRET_RESOURCE_EFFICENCY = "Resource Efficiency (Missile Turret)"
PLANETARY_FORTRESS_PROGRESSIVE_AUGMENTED_THRUSTERS = "Progressive Augmented Thrusters (Planetary Fortress)"
PLANETARY_FORTRESS_IBIKS_TRACKING_SCANNERS = "Ibiks Tracking Scanners (Planetary Fortress)"
PLANETARY_FORTRESS_ORBITAL_MODULE = "Orbital Module (Planetary Fortress)"
SENSOR_TOWER_ASSISTIVE_TARGETING = "Assistive Targeting (Sensor Tower)"
SENSOR_TOWER_MUILTISPECTRUM_DOPPLER = "Multispectrum Doppler (Sensor Tower)"
# Nova
NOVA_GHOST_VISOR = "Ghost Visor (Nova Equipment)"
NOVA_RANGEFINDER_OCULUS = "Rangefinder Oculus (Nova Equipment)"
NOVA_DOMINATION = "Domination (Nova Ability)"
NOVA_BLINK = "Blink (Nova Ability)"
NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE = "Progressive Stealth Suit Module (Nova Suit Module)"
NOVA_ENERGY_SUIT_MODULE = "Energy Suit Module (Nova Suit Module)"
NOVA_ARMORED_SUIT_MODULE = "Armored Suit Module (Nova Suit Module)"
NOVA_JUMP_SUIT_MODULE = "Jump Suit Module (Nova Suit Module)"
NOVA_C20A_CANISTER_RIFLE = "C20A Canister Rifle (Nova Weapon)"
NOVA_HELLFIRE_SHOTGUN = "Hellfire Shotgun (Nova Weapon)"
NOVA_PLASMA_RIFLE = "Plasma Rifle (Nova Weapon)"
NOVA_MONOMOLECULAR_BLADE = "Monomolecular Blade (Nova Weapon)"
NOVA_BLAZEFIRE_GUNBLADE = "Blazefire Gunblade (Nova Weapon)"
NOVA_STIM_INFUSION = "Stim Infusion (Nova Gadget)"
NOVA_PULSE_GRENADES = "Pulse Grenades (Nova Gadget)"
NOVA_FLASHBANG_GRENADES = "Flashbang Grenades (Nova Gadget)"
NOVA_IONIC_FORCE_FIELD = "Ionic Force Field (Nova Gadget)"
NOVA_HOLO_DECOY = "Holo Decoy (Nova Gadget)"
NOVA_NUKE = "Tac Nuke Strike (Nova Ability)"
# Zerg Units
ZERGLING = "Zergling"
SWARM_QUEEN = "Swarm Queen"
ROACH = "Roach"
HYDRALISK = "Hydralisk"
ABERRATION = "Aberration"
MUTALISK = "Mutalisk"
SWARM_HOST = "Swarm Host"
INFESTOR = "Infestor"
ULTRALISK = "Ultralisk"
PYGALISK = "Pygalisk"
CORRUPTOR = "Corruptor"
SCOURGE = "Scourge"
BROOD_QUEEN = "Brood Queen"
DEFILER = "Defiler"
INFESTED_MARINE = "Infested Marine"
INFESTED_SIEGE_TANK = "Infested Siege Tank"
INFESTED_DIAMONDBACK = "Infested Diamondback"
BULLFROG = "Bullfrog"
INFESTED_BANSHEE = "Infested Banshee"
INFESTED_LIBERATOR = "Infested Liberator"
# Zerg Buildings
SPORE_CRAWLER = "Spore Crawler"
SPINE_CRAWLER = "Spine Crawler"
BILE_LAUNCHER = "Bile Launcher"
INFESTED_BUNKER = "Infested Bunker"
INFESTED_MISSILE_TURRET = "Infested Missile Turret"
NYDUS_WORM = "Nydus Worm"
ECHIDNA_WORM = "Echidna Worm"
# Zerg Weapon / Armor Upgrades
ZERG_UPGRADE_PREFIX = "Progressive Zerg"
ZERG_FLYER_UPGRADE_PREFIX = f"{ZERG_UPGRADE_PREFIX} Flyer"
PROGRESSIVE_ZERG_MELEE_ATTACK = f"{ZERG_UPGRADE_PREFIX} Melee Attack"
PROGRESSIVE_ZERG_MISSILE_ATTACK = f"{ZERG_UPGRADE_PREFIX} Missile Attack"
PROGRESSIVE_ZERG_GROUND_CARAPACE = f"{ZERG_UPGRADE_PREFIX} Ground Carapace"
PROGRESSIVE_ZERG_FLYER_ATTACK = f"{ZERG_FLYER_UPGRADE_PREFIX} Attack"
PROGRESSIVE_ZERG_FLYER_CARAPACE = f"{ZERG_FLYER_UPGRADE_PREFIX} Carapace"
PROGRESSIVE_ZERG_WEAPON_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Weapon Upgrade"
PROGRESSIVE_ZERG_ARMOR_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Armor Upgrade"
PROGRESSIVE_ZERG_GROUND_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Ground Upgrade"
PROGRESSIVE_ZERG_FLYER_UPGRADE = f"{ZERG_FLYER_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Weapon/Armor Upgrade"
# Zerg Unit Upgrades
ZERGLING_HARDENED_CARAPACE = "Hardened Carapace (Zergling)"
ZERGLING_ADRENAL_OVERLOAD = "Adrenal Overload (Zergling)"
ZERGLING_METABOLIC_BOOST = "Metabolic Boost (Zergling)"
ZERGLING_SHREDDING_CLAWS = "Shredding Claws (Zergling)"
ROACH_HYDRIODIC_BILE = "Hydriodic Bile (Roach)"
ROACH_ADAPTIVE_PLATING = "Adaptive Plating (Roach)"
ROACH_TUNNELING_CLAWS = "Tunneling Claws (Roach)"
ROACH_GLIAL_RECONSTITUTION = "Glial Reconstitution (Roach)"
ROACH_ORGANIC_CARAPACE = "Organic Carapace (Roach)"
HYDRALISK_FRENZY = "Frenzy (Hydralisk)"
HYDRALISK_ANCILLARY_CARAPACE = "Ancillary Carapace (Hydralisk)"
HYDRALISK_GROOVED_SPINES = "Grooved Spines (Hydralisk)"
HYDRALISK_MUSCULAR_AUGMENTS = "Muscular Augments (Hydralisk)"
HYDRALISK_RESOURCE_EFFICIENCY = "Resource Efficiency (Hydralisk)"
BANELING_CORROSIVE_ACID = "Corrosive Acid (Baneling)"
BANELING_RUPTURE = "Rupture (Baneling)"
BANELING_REGENERATIVE_ACID = "Regenerative Acid (Baneling)"
BANELING_CENTRIFUGAL_HOOKS = "Centrifugal Hooks (Baneling)"
BANELING_TUNNELING_JAWS = "Tunneling Jaws (Baneling)"
BANELING_RAPID_METAMORPH = "Rapid Metamorph (Baneling)"
MUTALISK_VICIOUS_GLAIVE = "Vicious Glaive (Mutalisk)"
MUTALISK_RAPID_REGENERATION = "Rapid Regeneration (Mutalisk)"
MUTALISK_SUNDERING_GLAIVE = "Sundering Glaive (Mutalisk)"
MUTALISK_SEVERING_GLAIVE = "Severing Glaive (Mutalisk)"
MUTALISK_AERODYNAMIC_GLAIVE_SHAPE = "Aerodynamic Glaive Shape (Mutalisk)"
SPORE_CRAWLER_BIO_BONUS = "Caustic Enzymes (Spore Crawler)"
SWARM_HOST_BURROW = "Burrow (Swarm Host)"
SWARM_HOST_RAPID_INCUBATION = "Rapid Incubation (Swarm Host)"
SWARM_HOST_PRESSURIZED_GLANDS = "Pressurized Glands (Swarm Host)"
SWARM_HOST_LOCUST_METABOLIC_BOOST = "Locust Metabolic Boost (Swarm Host)"
SWARM_HOST_ENDURING_LOCUSTS = "Enduring Locusts (Swarm Host)"
SWARM_HOST_ORGANIC_CARAPACE = "Organic Carapace (Swarm Host)"
SWARM_HOST_RESOURCE_EFFICIENCY = "Resource Efficiency (Swarm Host)"
ULTRALISK_BURROW_CHARGE = "Burrow Charge (Ultralisk)"
ULTRALISK_TISSUE_ASSIMILATION = "Tissue Assimilation (Ultralisk)"
ULTRALISK_MONARCH_BLADES = "Monarch Blades (Ultralisk)"
ULTRALISK_ANABOLIC_SYNTHESIS = "Anabolic Synthesis (Ultralisk)"
ULTRALISK_CHITINOUS_PLATING = "Chitinous Plating (Ultralisk)"
ULTRALISK_ORGANIC_CARAPACE = "Organic Carapace (Ultralisk)"
ULTRALISK_RESOURCE_EFFICIENCY = "Resource Efficiency (Ultralisk)"
PYGALISK_STIM = "Stimpack (Pygalisk)"
PYGALISK_DUCAL_BLADES = "Ducal Blades (Pygalisk)"
PYGALISK_COMBAT_CARAPACE = "Combat Carapace (Pygalisk)"
CORRUPTOR_CORRUPTION = "Corruption (Corruptor)"
CORRUPTOR_CAUSTIC_SPRAY = "Caustic Spray (Corruptor)"
SCOURGE_VIRULENT_SPORES = "Virulent Spores (Scourge)"
SCOURGE_RESOURCE_EFFICIENCY = "Resource Efficiency (Scourge)"
SCOURGE_SWARM_SCOURGE = "Swarm Scourge (Scourge)"
DEVOURER_CORROSIVE_SPRAY = "Corrosive Spray (Devourer)"
DEVOURER_GAPING_MAW = "Gaping Maw (Devourer)"
DEVOURER_IMPROVED_OSMOSIS = "Improved Osmosis (Devourer)"
DEVOURER_PRESCIENT_SPORES = "Prescient Spores (Devourer)"
GUARDIAN_PROLONGED_DISPERSION = "Prolonged Dispersion (Guardian)"
GUARDIAN_PRIMAL_ADAPTATION = "Primal Adaptation (Guardian)"
GUARDIAN_SORONAN_ACID = "Soronan Acid (Guardian)"
GUARDIAN_PROPELLANT_SACS = "Propellant Sacs (Guardian)"
GUARDIAN_EXPLOSIVE_SPORES = "Explosive Spores (Guardian)"
GUARDIAN_PRIMORDIAL_FURY = "Primordial Fury (Guardian)"
IMPALER_ADAPTIVE_TALONS = "Adaptive Talons (Impaler)"
IMPALER_SECRETION_GLANDS = "Secretion Glands (Impaler)"
IMPALER_SUNKEN_SPINES = "Sunken Spines (Impaler)"
LURKER_SEISMIC_SPINES = "Seismic Spines (Lurker)"
LURKER_ADAPTED_SPINES = "Adapted Spines (Lurker)"
RAVAGER_POTENT_BILE = "Potent Bile (Ravager)"
RAVAGER_BLOATED_BILE_DUCTS = "Bloated Bile Ducts (Ravager)"
RAVAGER_DEEP_TUNNEL = "Deep Tunnel (Ravager)"
VIPER_PARASITIC_BOMB = "Parasitic Bomb (Viper)"
VIPER_PARALYTIC_BARBS = "Paralytic Barbs (Viper)"
VIPER_VIRULENT_MICROBES = "Virulent Microbes (Viper)"
BROOD_LORD_POROUS_CARTILAGE = "Porous Cartilage (Brood Lord)"
BROOD_LORD_BEHEMOTH_STELLARSKIN = "Behemoth Stellarskin (Brood Lord)"
BROOD_LORD_SPLITTER_MITOSIS = "Splitter Mitosis (Brood Lord)"
BROOD_LORD_RESOURCE_EFFICIENCY = "Resource Efficiency (Brood Lord)"
INFESTOR_INFESTED_TERRAN = "Infested Terran (Infestor)"
INFESTOR_MICROBIAL_SHROUD = "Microbial Shroud (Infestor)"
SWARM_QUEEN_SPAWN_LARVAE = "Spawn Larvae (Swarm Queen)"
SWARM_QUEEN_DEEP_TUNNEL = "Deep Tunnel (Swarm Queen)"
SWARM_QUEEN_ORGANIC_CARAPACE = "Organic Carapace (Swarm Queen)"
SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION = "Bio-Mechanical Transfusion (Swarm Queen)"
SWARM_QUEEN_RESOURCE_EFFICIENCY = "Resource Efficiency (Swarm Queen)"
SWARM_QUEEN_INCUBATOR_CHAMBER = "Incubator Chamber (Swarm Queen)"
BROOD_QUEEN_FUNGAL_GROWTH = "Fungal Growth (Brood Queen)"
BROOD_QUEEN_ENSNARE = "Ensnare (Brood Queen)"
BROOD_QUEEN_ENHANCED_MITOCHONDRIA = "Enhanced Mitochondria (Brood Queen)"
DEFILER_PATHOGEN_PROJECTORS = "Pathogen Projectors (Defiler)"
DEFILER_TRAPDOOR_ADAPTATION = "Trapdoor Adaptation (Defiler)"
DEFILER_PREDATORY_CONSUMPTION = "Predatory Consumption (Defiler)"
DEFILER_COMORBIDITY = "Comorbidity (Defiler)"
ABERRATION_MONSTROUS_RESILIENCE = "Monstrous Resilience (Aberration)"
ABERRATION_CONSTRUCT_REGENERATION = "Construct Regeneration (Aberration)"
ABERRATION_BANELING_INCUBATION = "Baneling Incubation (Aberration)"
ABERRATION_PROTECTIVE_COVER = "Protective Cover (Aberration)"
ABERRATION_RESOURCE_EFFICIENCY = "Resource Efficiency (Aberration)"
ABERRATION_PROGRESSIVE_BANELING_LAUNCH = "Progressive Baneling Launch (Aberration)"
CORRUPTOR_MONSTROUS_RESILIENCE = "Monstrous Resilience (Corruptor)"
CORRUPTOR_CONSTRUCT_REGENERATION = "Construct Regeneration (Corruptor)"
CORRUPTOR_SCOURGE_INCUBATION = "Scourge Incubation (Corruptor)"
CORRUPTOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Corruptor)"
PRIMAL_IGNITER_CONCENTRATED_FIRE = "Concentrated Fire (Primal Igniter)"
PRIMAL_IGNITER_PRIMAL_TENACITY = "Primal Tenacity (Primal Igniter)"
OVERLORD_IMPROVED_OVERLORDS = "Improved Overlords (Overlord)"
OVERLORD_VENTRAL_SACS = "Ventral Sacs (Overlord)"
OVERLORD_GENERATE_CREEP = "Generate Creep (Overlord)"
OVERLORD_PNEUMATIZED_CARAPACE = "Pneumatized Carapace (Overlord)"
OVERLORD_ANTENNAE = "Antennae (Overlord)"
INFESTED_SCV_BUILD_CHARGES = "Sustained Cultivation Ventricles (Infested SCV)"
INFESTED_MARINE_PLAGUED_MUNITIONS = "Plagued Munitions (Infested Marine)"
INFESTED_MARINE_RETINAL_AUGMENTATION = "Retinal Augmentation (Infested Marine)"
INFESTED_BUNKER_CALCIFIED_ARMOR = "Calcified Armor (Infested Bunker)"
INFESTED_BUNKER_REGENERATIVE_PLATING = "Regenerative Plating (Infested Bunker)"
INFESTED_BUNKER_ENGORGED_BUNKERS = "Engorged Bunkers (Infested Bunker)"
TYRANNOZOR_BARRAGE_OF_SPIKES = "Barrage of Spikes (Tyrannozor)"
TYRANNOZOR_TYRANTS_PROTECTION = "Tyrant's Protection (Tyrannozor)"
TYRANNOZOR_HEALING_ADAPTATION = "Healing Adaptation (Tyrannozor)"
TYRANNOZOR_IMPALING_STRIKE = "Impaling Strike (Tyrannozor)"
BILE_LAUNCHER_ARTILLERY_DUCTS = "Artillery Ducts (Bile Launcher)"
BILE_LAUNCHER_RAPID_BOMBARMENT = "Rapid Bombardment (Bile Launcher)"
NYDUS_WORM_ECHIDNA_WORM_SUBTERRANEAN_SCALES = "Subterranean Scales (Nydus Worm/Echidna Worm)"
NYDUS_WORM_ECHIDNA_WORM_JORMUNGANDR_STRAIN = "Jormungandr Strain (Nydus Worm/Echidna Worm)"
NYDUS_WORM_ECHIDNA_WORM_RESOURCE_EFFICIENCY = "Resource Efficiency (Nydus Worm/Echidna Worm)"
NYDUS_WORM_RAVENOUS_APPETITE = "Ravenous Appetite (Nydus Worm)"
ECHIDNA_WORM_OUROBOROS_STRAIN = "Ouroboros Strain (Echidna Worm)"
INFESTED_SIEGE_TANK_PROGRESSIVE_AUTOMATED_MITOSIS = "Progressive Automated Mitosis (Infested Siege Tank)"
INFESTED_SIEGE_TANK_ACIDIC_ENZYMES = "Acidic Enzymes (Infested Siege Tank)"
INFESTED_SIEGE_TANK_DEEP_TUNNEL = "Deep Tunnel (Infested Siege Tank)"
INFESTED_SIEGE_TANK_SEISMIC_SONAR = "Seismic Sonar (Infested Siege Tank)"
INFESTED_SIEGE_TANK_BALANCED_ROOTS = "Balanced Roots (Infested Siege Tank)"
INFESTED_DIAMONDBACK_CAUSTIC_MUCUS = "Caustic Mucus (Infested Diamondback)"
INFESTED_DIAMONDBACK_VIOLENT_ENZYMES = "Violent Enzymes (Infested Diamondback)"
INFESTED_DIAMONDBACK_CONCENTRATED_SPEW = "Concentrated Spew (Infested Diamondback)"
INFESTED_DIAMONDBACK_PROGRESSIVE_FUNGAL_SNARE = "Progressive Fungal Snare (Infested Diamondback)"
INFESTED_BANSHEE_BRACED_EXOSKELETON = "Braced Exoskeleton (Infested Banshee)"
INFESTED_BANSHEE_RAPID_HIBERNATION = "Rapid Hibernation (Infested Banshee)"
INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS = "Fleshfused Targeting Optics (Infested Banshee)"
INFESTED_LIBERATOR_CLOUD_DISPERSAL = "Cloud Dispersal (Infested Liberator)"
INFESTED_LIBERATOR_VIRAL_CONTAMINATION = "Viral Contamination (Infested Liberator)"
INFESTED_LIBERATOR_DEFENDER_MODE = "Defender Mode (Infested Liberator)"
INFESTED_SIEGE_TANK_FRIGHTFUL_FLESHWELDER = "Frightful Fleshwelder (Infested Siege Tank)"
INFESTED_DIAMONDBACK_FRIGHTFUL_FLESHWELDER = "Frightful Fleshwelder (Infested Diamondback)"
INFESTED_BANSHEE_FRIGHTFUL_FLESHWELDER = "Frightful Fleshwelder (Infested Banshee)"
INFESTED_LIBERATOR_FRIGHTFUL_FLESHWELDER = "Frightful Fleshwelder (Infested Liberator)"
INFESTED_MISSILE_TURRET_BIOELECTRIC_PAYLOAD = "Bioelectric Payload (Infested Missile Turret)"
INFESTED_MISSILE_TURRET_ACID_SPORE_VENTS = "Acid Spore Vents (Infested Missile Turret)"
BULLFROG_WILD_MUTATION = "Mutagen Vents (Bullfrog)"
BULLFROG_BROODLINGS = "Suffused With Vermin (Bullfrog)"
BULLFROG_HARD_IMPACT = "Lethal Impact (Bullfrog)"
BULLFROG_RANGE = "Catalytic Boosters (Bullfrog)"
# Zerg Strains
ZERGLING_RAPTOR_STRAIN = "Raptor Strain (Zergling)"
ZERGLING_SWARMLING_STRAIN = "Swarmling Strain (Zergling)"
ROACH_VILE_STRAIN = "Vile Strain (Roach)"
ROACH_CORPSER_STRAIN = "Corpser Strain (Roach)"
BANELING_SPLITTER_STRAIN = "Splitter Strain (Baneling)"
BANELING_HUNTER_STRAIN = "Hunter Strain (Baneling)"
SWARM_HOST_CARRION_STRAIN = "Carrion Strain (Swarm Host)"
SWARM_HOST_CREEPER_STRAIN = "Creeper Strain (Swarm Host)"
ULTRALISK_NOXIOUS_STRAIN = "Noxious Strain (Ultralisk)"
ULTRALISK_TORRASQUE_STRAIN = "Torrasque Strain (Ultralisk)"
# Morphs
ZERGLING_BANELING_ASPECT = "Baneling"
HYDRALISK_IMPALER_ASPECT = "Impaler"
HYDRALISK_LURKER_ASPECT = "Lurker"
MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT = "Brood Lord"
MUTALISK_CORRUPTOR_VIPER_ASPECT = "Viper"
MUTALISK_CORRUPTOR_GUARDIAN_ASPECT = "Guardian"
MUTALISK_CORRUPTOR_DEVOURER_ASPECT = "Devourer"
ROACH_RAVAGER_ASPECT = "Ravager"
OVERLORD_OVERSEER_ASPECT = "Overseer"
ROACH_PRIMAL_IGNITER_ASPECT = "Primal Igniter"
ULTRALISK_TYRANNOZOR_ASPECT = "Tyrannozor"
# Zerg Mercs
INFESTED_MEDICS = "Infested Medics"
INFESTED_SIEGE_BREAKERS = "Infested Siege Breakers"
INFESTED_DUSK_WINGS = "Infested Dusk Wings"
DEVOURING_ONES = "Devouring Ones"
HUNTER_KILLERS = "Hunter Killers"
TORRASQUE_MERC = "Wise Old Torrasque"
HUNTERLING = "Hunterling"
YGGDRASIL = "Yggdrasil"
CAUSTIC_HORRORS = "Caustic Horrors"
# Kerrigan Upgrades
KERRIGAN_KINETIC_BLAST = "Kinetic Blast (Kerrigan Ability)"
KERRIGAN_HEROIC_FORTITUDE = "Heroic Fortitude (Kerrigan Passive)"
KERRIGAN_LEAPING_STRIKE = "Leaping Strike (Kerrigan Ability)"
KERRIGAN_CRUSHING_GRIP = "Crushing Grip (Kerrigan Ability)"
KERRIGAN_CHAIN_REACTION = "Chain Reaction (Kerrigan Passive)"
KERRIGAN_PSIONIC_SHIFT = "Psionic Shift (Kerrigan Ability)"
KERRIGAN_WILD_MUTATION = "Wild Mutation (Kerrigan Ability)"
KERRIGAN_SPAWN_BANELINGS = "Spawn Banelings (Kerrigan Ability)"
KERRIGAN_MEND = "Mend (Kerrigan Ability)"
KERRIGAN_INFEST_BROODLINGS = "Infest Broodlings (Kerrigan Passive)"
KERRIGAN_FURY = "Fury (Kerrigan Passive)"
KERRIGAN_ABILITY_EFFICIENCY = "Ability Efficiency (Kerrigan Passive)"
KERRIGAN_APOCALYPSE = "Apocalypse (Kerrigan Ability)"
KERRIGAN_SPAWN_LEVIATHAN = "Spawn Leviathan (Kerrigan Ability)"
KERRIGAN_DROP_PODS = "Drop-Pods (Kerrigan Ability)"
KERRIGAN_ASSIMILATION_AURA = "Assimilation Aura (Kerrigan Ability)"
KERRIGAN_IMMOBILIZATION_WAVE = "Immobilization Wave (Kerrigan Ability)"
KERRIGAN_PRIMAL_FORM = "Primal Form (Kerrigan)"
# Misc Upgrades
ZERGLING_RECONSTITUTION = "Zergling Reconstitution (Zerg)"
AUTOMATED_EXTRACTORS = "Automated Extractors (Zerg)"
TWIN_DRONES = "Twin Drones (Zerg)"
MALIGNANT_CREEP = "Malignant Creep (Zerg)"
VESPENE_EFFICIENCY = "Vespene Efficiency (Zerg)"
ZERG_CREEP_STOMACH = "Creep Stomach (Zerg)"
ZERG_EXCAVATING_CLAWS = "Excavating Claws (Zerg)"
HIVE_CLUSTER_MATURATION = "Hive Cluster Maturation (Zerg)"
MACROSCOPIC_RECUPERATION = "Macroscopic Recuperation (Zerg)"
BIOMECHANICAL_STOCKPILING = "Bio-Mechanical Stockpiling (Zerg)"
BROODLING_SPORE_SATURATION = "Broodling Spore Saturation (Zerg)"
UNRESTRICTED_MUTATION = "Unrestricted Mutation (Zerg)"
CELL_DIVISION = "Cell Division (Zerg)"
EVOLUTIONARY_LEAP = "Evolutionary Leap (Zerg)"
SELF_SUFFICIENT = "Self-Sufficient (Zerg)"
# Kerrigan Levels
KERRIGAN_LEVELS_1 = "1 Kerrigan Level"
KERRIGAN_LEVELS_2 = "2 Kerrigan Levels"
KERRIGAN_LEVELS_3 = "3 Kerrigan Levels"
KERRIGAN_LEVELS_4 = "4 Kerrigan Levels"
KERRIGAN_LEVELS_5 = "5 Kerrigan Levels"
KERRIGAN_LEVELS_6 = "6 Kerrigan Levels"
KERRIGAN_LEVELS_7 = "7 Kerrigan Levels"
KERRIGAN_LEVELS_8 = "8 Kerrigan Levels"
KERRIGAN_LEVELS_9 = "9 Kerrigan Levels"
KERRIGAN_LEVELS_10 = "10 Kerrigan Levels"
KERRIGAN_LEVELS_14 = "14 Kerrigan Levels"
KERRIGAN_LEVELS_35 = "35 Kerrigan Levels"
KERRIGAN_LEVELS_70 = "70 Kerrigan Levels"
# Protoss Units
ZEALOT = "Zealot"
STALKER = "Stalker"
HIGH_TEMPLAR = "High Templar"
DARK_TEMPLAR = "Dark Templar"
IMMORTAL = "Immortal"
COLOSSUS = "Colossus"
PHOENIX = "Phoenix"
VOID_RAY = "Void Ray"
CARRIER = "Carrier"
SKYLORD = "Skylord"
TRIREME = "Trireme"
OBSERVER = "Observer"
CENTURION = "Centurion"
SENTINEL = "Sentinel"
SUPPLICANT = "Supplicant"
INSTIGATOR = "Instigator"
SLAYER = "Slayer"
SENTRY = "Sentry"
ENERGIZER = "Energizer"
HAVOC = "Havoc"
SIGNIFIER = "Signifier"
ASCENDANT = "Ascendant"
AVENGER = "Avenger"
BLOOD_HUNTER = "Blood Hunter"
DRAGOON = "Dragoon"
DARK_ARCHON = "Dark Archon"
ADEPT = "Adept"
WARP_PRISM = "Warp Prism"
ANNIHILATOR = "Annihilator"
VANGUARD = "Vanguard"
STALWART = "Stalwart"
WRATHWALKER = "Wrathwalker"
REAVER = "Reaver"
DISRUPTOR = "Disruptor"
MIRAGE = "Mirage"
SKIRMISHER = "Skirmisher"
CORSAIR = "Corsair"
DESTROYER = "Destroyer"
PULSAR = "Pulsar"
DAWNBRINGER = "Dawnbringer"
SCOUT = "Scout"
OPPRESSOR = "Oppressor"
CALADRIUS = "Caladrius"
MISTWING = "Mist Wing"
TEMPEST = "Tempest"
MOTHERSHIP = "Mothership"
ARBITER = "Arbiter"
ORACLE = "Oracle"
# Upgrades
PROTOSS_UPGRADE_PREFIX = "Progressive Protoss"
PROTOSS_GROUND_UPGRADE_PREFIX = f"{PROTOSS_UPGRADE_PREFIX} Ground"
PROTOSS_AIR_UPGRADE_PREFIX = f"{PROTOSS_UPGRADE_PREFIX} Air"
PROGRESSIVE_PROTOSS_GROUND_WEAPON = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_PROTOSS_GROUND_ARMOR = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Armor"
PROGRESSIVE_PROTOSS_SHIELDS = f"{PROTOSS_UPGRADE_PREFIX} Shields"
PROGRESSIVE_PROTOSS_AIR_WEAPON = f"{PROTOSS_AIR_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_PROTOSS_AIR_ARMOR = f"{PROTOSS_AIR_UPGRADE_PREFIX} Armor"
PROGRESSIVE_PROTOSS_WEAPON_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Weapon Upgrade"
PROGRESSIVE_PROTOSS_ARMOR_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Armor Upgrade"
PROGRESSIVE_PROTOSS_GROUND_UPGRADE = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_PROTOSS_AIR_UPGRADE = f"{PROTOSS_AIR_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Weapon/Armor Upgrade"
# Buildings
PHOTON_CANNON = "Photon Cannon"
KHAYDARIN_MONOLITH = "Khaydarin Monolith"
SHIELD_BATTERY = "Shield Battery"
# Unit Upgrades
SUPPLICANT_BLOOD_SHIELD = "Blood Shield (Supplicant)"
SUPPLICANT_SOUL_AUGMENTATION = "Soul Augmentation (Supplicant)"
SUPPLICANT_ENDLESS_SERVITUDE = "Endless Servitude (Supplicant)"
SUPPLICANT_ZENITH_PITCH = "Zenith Pitch (Supplicant)"
SUPPLICANT_SACRIFICE = "Sacrifice (Supplicant)"
ADEPT_SHOCKWAVE = "Shockwave (Adept)"
ADEPT_RESONATING_GLAIVES = "Resonating Glaives (Adept)"
ADEPT_PHASE_BULWARK = "Phase Bulwark (Adept)"
STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES = "Disintegrating Particles (Stalker/Instigator/Slayer)"
STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION = "Particle Reflection (Stalker/Instigator/Slayer)"
INSTIGATOR_BLINK_OVERDRIVE = "Blink Overdrive (Instigator)"
INSTIGATOR_RECONSTRUCTION = "Reconstruction (Instigator)"
DRAGOON_CONCENTRATED_ANTIMATTER = "Concentrated Antimatter (Dragoon)"
DRAGOON_TRILLIC_COMPRESSION_SYSTEM = "Trillic Compression System (Dragoon)"
DRAGOON_SINGULARITY_CHARGE = "Singularity Charge (Dragoon)"
DRAGOON_ENHANCED_STRIDER_SERVOS = "Enhanced Strider Servos (Dragoon)"
SCOUT_COMBAT_SENSOR_ARRAY = "Combat Sensor Array (Scout/Oppressor/Caladrius/Mist Wing)"
SCOUT_APIAL_SENSORS = "Apial Sensors (Scout)"
SCOUT_GRAVITIC_THRUSTERS = "Gravitic Thrusters (Scout/Oppressor/Caladrius/Mist Wing)"
SCOUT_ADVANCED_PHOTON_BLASTERS = "Advanced Photon Blasters (Scout/Oppressor/Mist Wing)"
SCOUT_RESOURCE_EFFICIENCY = "Resource Efficiency (Scout)"
SCOUT_SUPPLY_EFFICIENCY = "Supply Efficiency (Scout)"
TEMPEST_TECTONIC_DESTABILIZERS = "Tectonic Destabilizers (Tempest)"
TEMPEST_QUANTIC_REACTOR = "Quantic Reactor (Tempest)"
TEMPEST_GRAVITY_SLING = "Gravity Sling (Tempest)"
TEMPEST_INTERPLANETARY_RANGE = "Interplanetary Range (Tempest)"
PHOENIX_CLASS_IONIC_WAVELENGTH_FLUX = "Ionic Wavelength Flux (Phoenix/Mirage/Skirmisher)"
PHOENIX_CLASS_ANION_PULSE_CRYSTALS = "Anion Pulse-Crystals (Phoenix/Mirage/Skirmisher)"
CORSAIR_STEALTH_DRIVE = "Stealth Drive (Corsair)"
CORSAIR_ARGUS_JEWEL = "Argus Jewel (Corsair)"
CORSAIR_SUSTAINING_DISRUPTION = "Sustaining Disruption (Corsair)"
CORSAIR_NEUTRON_SHIELDS = "Neutron Shields (Corsair)"
ORACLE_STEALTH_DRIVE = "Stealth Drive (Oracle)"
ORACLE_SKYWARD_CHRONOANOMALY = "Skyward Chronoanomaly (Oracle)"
ORACLE_TEMPORAL_ACCELERATION_BEAM = "Temporal Acceleration Beam (Oracle)"
ORACLE_BOSONIC_CORE = "Bosonic Core (Oracle)"
ARBITER_CHRONOSTATIC_REINFORCEMENT = "Chronostatic Reinforcement (Arbiter)"
ARBITER_KHAYDARIN_CORE = "Khaydarin Core (Arbiter)"
ARBITER_SPACETIME_ANCHOR = "Spacetime Anchor (Arbiter)"
ARBITER_RESOURCE_EFFICIENCY = "Resource Efficiency (Arbiter)"
ARBITER_JUDICATORS_VEIL = "Judicator's Veil (Arbiter)"
CARRIER_TRIREME_GRAVITON_CATAPULT = "Graviton Catapult (Carrier/Trireme)"
CARRIER_SKYLORD_TRIREME_HULL_OF_PAST_GLORIES = "Hull of Past Glories (Carrier/Skylord/Trireme)"
VOID_RAY_DESTROYER_PULSAR_DAWNBRINGER_FLUX_VANES = "Flux Vanes (Void Ray/Destroyer/Pulsar/Dawnbringer)"
DAWNBRINGER_ANTI_SURFACE_COUNTERMEASURES = "Anti-Surface Countermeasures (Dawnbringer)"
DAWNBRINGER_ENHANCED_SHIELD_GENERATOR = "Enhanced Shield Generator (Dawnbringer)"
PULSAR_CHRONOCLYSM = "Chronoclysm (Pulsar)"
PULSAR_ENTROPIC_REVERSAL = "Entropic Reversal (Pulsar)"
DESTROYER_RESOURCE_EFFICIENCY = "Resource Efficiency (Destroyer)"
WARP_PRISM_GRAVITIC_DRIVE = "Gravitic Drive (Warp Prism)"
WARP_PRISM_PHASE_BLASTER = "Phase Blaster (Warp Prism)"
WARP_PRISM_WAR_CONFIGURATION = "War Configuration (Warp Prism)"
OBSERVER_GRAVITIC_BOOSTERS = "Gravitic Boosters (Observer)"
OBSERVER_SENSOR_ARRAY = "Sensor Array (Observer)"
REAVER_SCARAB_DAMAGE = "Scarab Damage (Reaver)"
REAVER_SOLARITE_PAYLOAD = "Solarite Payload (Reaver)"
REAVER_REAVER_CAPACITY = "Reaver Capacity (Reaver)"
REAVER_RESOURCE_EFFICIENCY = "Resource Efficiency (Reaver)"
REAVER_BARGAIN_BIN_PRICES = "Bargain Bin Prices (Reaver)"
VANGUARD_AGONY_LAUNCHERS = "Agony Launchers (Vanguard)"
VANGUARD_MATTER_DISPERSION = "Matter Dispersion (Vanguard)"
IMMORTAL_ANNIHILATOR_SINGULARITY_CHARGE = "Singularity Charge (Immortal/Annihilator)"
IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING = "Advanced Targeting (Immortal/Annihilator)"
IMMORTAL_ANNIHILATOR_DISRUPTOR_DISPERSION = "Disruptor Dispersion (Immortal/Annihilator)"
STALWART_HIGH_VOLTAGE_CAPACITORS = "High Voltage Capacitors (Stalwart)"
STALWART_REINTEGRATED_FRAMEWORK = "Reintegrated Framework (Stalwart)"
STALWART_STABILIZED_ELECTRODES = "Stabilized Electrodes (Stalwart)"
STALWART_LATTICED_SHIELDING = "Latticed Shielding (Stalwart)"
DISRUPTOR_CLOAKING_MODULE = "Cloaking Module (Disruptor)"
DISRUPTOR_PERFECTED_POWER = "Perfected Power (Disruptor)"
DISRUPTOR_RESTRAINED_DESTRUCTION = "Restrained Destruction (Disruptor)"
COLOSSUS_PACIFICATION_PROTOCOL = "Pacification Protocol (Colossus)"
WRATHWALKER_RAPID_POWER_CYCLING = "Rapid Power Cycling (Wrathwalker)"
WRATHWALKER_EYE_OF_WRATH = "Eye of Wrath (Wrathwalker)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHROUD_OF_ADUN = "Shroud of Adun (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHADOW_GUARD_TRAINING = "Shadow Guard Training (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK = "Blink (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_RESOURCE_EFFICIENCY = "Resource Efficiency (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_DARK_ARCHON_MELD = "Dark Archon Meld (Dark Templar)"
DARK_TEMPLAR_ARCHON_MERGE = "Archon Merge (Dark Templar)"
HIGH_TEMPLAR_SIGNIFIER_UNSHACKLED_PSIONIC_STORM = "Unshackled Psionic Storm (High Templar/Signifier)"
HIGH_TEMPLAR_SIGNIFIER_HALLUCINATION = "Hallucination (High Templar/Signifier)"
HIGH_TEMPLAR_SIGNIFIER_KHAYDARIN_AMULET = "Khaydarin Amulet (High Templar/Signifier)"
ARCHON_HIGH_ARCHON = "High Archon (Archon)"
ARCHON_TRANSCENDENCE = "Transcendence (Archon)"
ARCHON_POWER_SIPHON = "Power Siphon (Archon)"
ARCHON_ERADICATE = "Eradicate (Archon)"
ARCHON_OBLITERATE = "Obliterate (Archon)"
DARK_ARCHON_FEEDBACK = "Feedback (Dark Archon)"
DARK_ARCHON_MAELSTROM = "Maelstrom (Dark Archon)"
DARK_ARCHON_ARGUS_TALISMAN = "Argus Talisman (Dark Archon)"
ASCENDANT_POWER_OVERWHELMING = "Power Overwhelming (Ascendant)"
ASCENDANT_CHAOTIC_ATTUNEMENT = "Chaotic Attunement (Ascendant)"
ASCENDANT_BLOOD_AMULET = "Blood Amulet (Ascendant)"
ASCENDANT_ARCHON_MERGE = "Archon Merge (Ascendant)"
SENTRY_ENERGIZER_HAVOC_CLOAKING_MODULE = "Cloaking Module (Sentry/Energizer/Havoc)"
SENTRY_ENERGIZER_HAVOC_SHIELD_BATTERY_RAPID_RECHARGING = "Rapid Recharging (Sentry/Energizer/Havoc/Shield Battery)"
SENTRY_FORCE_FIELD = "Force Field (Sentry)"
SENTRY_HALLUCINATION = "Hallucination (Sentry)"
ENERGIZER_RECLAMATION = "Reclamation (Energizer)"
ENERGIZER_FORGED_CHASSIS = "Forged Chassis (Energizer)"
HAVOC_DETECT_WEAKNESS = "Detect Weakness (Havoc)"
HAVOC_BLOODSHARD_RESONANCE = "Bloodshard Resonance (Havoc)"
ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS = "Leg Enhancements (Zealot/Sentinel/Centurion)"
ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY = "Shield Capacity (Zealot/Sentinel/Centurion)"
OPPRESSOR_ACCELERATED_WARP = "Accelerated Warp (Oppressor)"
OPPRESSOR_ARMOR_MELTING_BLASTERS = "Armor Melting Blasters (Oppressor)"
CALADRIUS_SIDE_MISSILES = "Side Missiles (Caladrius)"
CALADRIUS_STRUCTURE_TARGETING = "Structure Targeting (Caladrius)"
CALADRIUS_SOLARITE_REACTOR = "Solarite Reactor (Caladrius)"
MISTWING_NULL_SHROUD = "Null Shroud (Mist Wing)"
MISTWING_PILOT = "Pilot (Mist Wing)"
# War Council
ZEALOT_WHIRLWIND = "Whirlwind (Zealot)"
CENTURION_RESOURCE_EFFICIENCY = "Resource Efficiency (Centurion)"
SENTINEL_RESOURCE_EFFICIENCY = "Resource Efficiency (Sentinel)"
STALKER_PHASE_REACTOR = "Phase Reactor (Stalker)"
DRAGOON_PHALANX_SUIT = "Phalanx Suit (Dragoon)"
INSTIGATOR_MODERNIZED_SERVOS = "Modernized Servos (Instigator)"
ADEPT_DISRUPTIVE_TRANSFER = "Disruptive Transfer (Adept)"
SLAYER_PHASE_BLINK = "Phase Blink (Slayer)"
AVENGER_KRYHAS_CLOAK = "Kryhas Cloak (Avenger)"
DARK_TEMPLAR_LESSER_SHADOW_FURY = "Lesser Shadow Fury (Dark Templar)"
DARK_TEMPLAR_GREATER_SHADOW_FURY = "Greater Shadow Fury (Dark Templar)"
BLOOD_HUNTER_BRUTAL_EFFICIENCY = "Brutal Efficiency (Blood Hunter)"
SENTRY_DOUBLE_SHIELD_RECHARGE = "Double Shield Recharge (Sentry)"
ENERGIZER_MOBILE_CHRONO_BEAM = "Mobile Chrono Beam (Energizer)"
HAVOC_ENDURING_SIGHT = "Enduring Sight (Havoc)"
HIGH_TEMPLAR_PLASMA_SURGE = "Plasma Surge (High Templar)"
SIGNIFIER_FEEDBACK = "Feedback (Signifier)"
ASCENDANT_BREATH_OF_CREATION = "Breath of Creation (Ascendant)"
DARK_ARCHON_INDOMITABLE_WILL = "Indomitable Will (Dark Archon)"
IMMORTAL_IMPROVED_BARRIER = "Improved Barrier (Immortal)"
VANGUARD_RAPIDFIRE_CANNON = "Rapid-Fire Cannon (Vanguard)"
VANGUARD_FUSION_MORTARS = "Fusion Mortars (Vanguard)"
ANNIHILATOR_TWILIGHT_CHASSIS = "Twilight Chassis (Annihilator)"
STALWART_ARC_INDUCERS = "Arc Inducers (Stalwart)"
COLOSSUS_FIRE_LANCE = "Fire Lance (Colossus)"
WRATHWALKER_AERIAL_TRACKING = "Aerial Tracking (Wrathwalker)"
REAVER_KHALAI_REPLICATORS = "Khalai Replicators (Reaver)"
DISRUPTOR_MOBILITY_PROTOCOLS = "Mobility Protocols (Disruptor)"
WARP_PRISM_WARP_REFRACTION = "Warp Refraction (Warp Prism)"
OBSERVER_INDUCE_SCOPOPHOBIA = "Induce Scopophobia (Observer)"
PHOENIX_DOUBLE_GRAVITON_BEAM = "Double Graviton Beam (Phoenix)"
CORSAIR_NETWORK_DISRUPTION = "Network Disruption (Corsair)"
MIRAGE_GRAVITON_BEAM = "Graviton Beam (Mirage)"
SKIRMISHER_PEER_CONTEMPT = "Peer Contempt (Skirmisher)"
VOID_RAY_PRISMATIC_RANGE = "Prismatic Range (Void Ray)"
DESTROYER_REFORGED_BLOODSHARD_CORE = "Reforged Bloodshard Core (Destroyer)"
PULSAR_CHRONO_SHEAR = "Chrono Shear (Pulsar)"
DAWNBRINGER_SOLARITE_LENS = "Solarite Lens (Dawnbringer)"
CARRIER_REPAIR_DRONES = "Repair Drones (Carrier)"
SKYLORD_JUMP = "Jump (Skylord)"
TRIREME_SOLAR_BEAM = "Solar Beam (Trireme)"
TEMPEST_DISINTEGRATION = "Disintegration (Tempest)"
SCOUT_EXPEDITIONARY_HULL = "Expeditionary Hull (Scout)"
ARBITER_VESSEL_OF_THE_CONCLAVE = "Vessel of the Conclave (Arbiter)"
ORACLE_STASIS_CALIBRATION = "Stasis Calibration (Oracle)"
MOTHERSHIP_INTEGRATED_POWER = "Integrated Power (Mothership)"
OPPRESSOR_VULCAN_BLASTER = "Vulcan Blaster (Oppressor)"
CALADRIUS_CORONA_BEAM = "Corona Beam (Caladrius)"
MISTWING_PHANTOM_DASH = "Phantom Dash (Mist Wing)"
# Spear Of Adun
SOA_CHRONO_SURGE = "Chrono Surge (Spear of Adun)"
SOA_PROGRESSIVE_PROXY_PYLON = "Progressive Proxy Pylon (Spear of Adun)"
SOA_PYLON_OVERCHARGE = "Pylon Overcharge (Spear of Adun)"
SOA_ORBITAL_STRIKE = "Orbital Strike (Spear of Adun)"
SOA_TEMPORAL_FIELD = "Temporal Field (Spear of Adun)"
SOA_SOLAR_LANCE = "Solar Lance (Spear of Adun)"
SOA_MASS_RECALL = "Mass Recall (Spear of Adun)"
SOA_SHIELD_OVERCHARGE = "Shield Overcharge (Spear of Adun)"
SOA_DEPLOY_FENIX = "Deploy Fenix (Spear of Adun)"
SOA_PURIFIER_BEAM = "Purifier Beam (Spear of Adun)"
SOA_TIME_STOP = "Time Stop (Spear of Adun)"
SOA_SOLAR_BOMBARDMENT = "Solar Bombardment (Spear of Adun)"
# Generic upgrades
MATRIX_OVERLOAD = "Matrix Overload (Protoss)"
QUATRO = "Quatro (Protoss)"
NEXUS_OVERCHARGE = "Nexus Overcharge (Protoss)"
ORBITAL_ASSIMILATORS = "Orbital Assimilators (Protoss)"
WARP_HARMONIZATION = "Warp Harmonization (Protoss)"
GUARDIAN_SHELL = "Guardian Shell (Spear of Adun)"
RECONSTRUCTION_BEAM = "Reconstruction Beam (Spear of Adun)"
OVERWATCH = "Overwatch (Spear of Adun)"
SUPERIOR_WARP_GATES = "Superior Warp Gates (Protoss)"
ENHANCED_TARGETING = "Enhanced Targeting (Protoss)"
OPTIMIZED_ORDNANCE = "Optimized Ordnance (Protoss)"
KHALAI_INGENUITY = "Khalai Ingenuity (Protoss)"
AMPLIFIED_ASSIMILATORS = "Amplified Assimilators (Protoss)"
PROGRESSIVE_WARP_RELOCATE = "Progressive Warp Relocate (Protoss)"
PROBE_WARPIN = "Probe Warp-In (Protoss)"
ELDER_PROBES = "Elder Probes (Protoss)"
# Filler items
STARTING_MINERALS = "Additional Starting Minerals"
STARTING_VESPENE = "Additional Starting Vespene"
STARTING_SUPPLY = "Additional Starting Supply"
MAX_SUPPLY = "Additional Maximum Supply"
SHIELD_REGENERATION = "Increased Shield Regeneration"
BUILDING_CONSTRUCTION_SPEED = "Increased Building Construction Speed"
UPGRADE_RESEARCH_SPEED = "Increased Upgrade Research Speed"
UPGRADE_RESEARCH_COST = "Reduced Upgrade Research Cost"
# Trap
REDUCED_MAX_SUPPLY = "Decreased Maximum Supply"
NOTHING = "Nothing"
# Deprecated
PROGRESSIVE_ORBITAL_COMMAND = "Progressive Orbital Command (Deprecated)"
# Keys
_TEMPLATE_MISSION_KEY = "{} Mission Key"
_TEMPLATE_NAMED_LAYOUT_KEY = "{} ({}) Questline Key"
_TEMPLATE_NUMBERED_LAYOUT_KEY = "Questline Key #{}"
_TEMPLATE_NAMED_CAMPAIGN_KEY = "{} Campaign Key"
_TEMPLATE_NUMBERED_CAMPAIGN_KEY = "Campaign Key #{}"
_TEMPLATE_FLAVOR_KEY = "{} Key"
PROGRESSIVE_MISSION_KEY = "Progressive Mission Key"
PROGRESSIVE_QUESTLINE_KEY = "Progressive Questline Key"
_TEMPLATE_PROGRESSIVE_KEY = "Progressive Key #{}"
# Names for flavor keys, feel free to add more, but add them to the Custom Mission Order docs too
# These will never be randomly created by the generator
_flavor_key_names = [
"Terran", "Zerg", "Protoss",
"Raynor", "Tychus", "Swann", "Stetmann", "Hanson", "Nova", "Tosh", "Valerian", "Warfield", "Mengsk", "Han", "Horner",
"Kerrigan", "Zagara", "Abathur", "Yagdra", "Kraith", "Slivan", "Zurvan", "Brakk", "Stukov", "Dehaka", "Niadra", "Izsha",
"Artanis", "Zeratul", "Tassadar", "Karax", "Vorazun", "Alarak", "Fenix", "Urun", "Mohandar", "Selendis", "Rohana",
"Reigel", "Davis", "Ji'nara"
]

View File

@@ -0,0 +1,266 @@
"""
Utilities for telling item parentage hierarchy.
ItemData in item_tables.py will point from child item -> parent rule.
Rules have a `parent_items()` method which links rule -> parent items.
Rules may be more complex than all or any items being present. Call them to determine if they are satisfied.
"""
from typing import Dict, List, Iterable, Sequence, Optional, TYPE_CHECKING
import abc
from . import item_names, parent_names, item_tables, item_groups
if TYPE_CHECKING:
from ..options import Starcraft2Options
class PresenceRule(abc.ABC):
"""Contract for a parent presence rule. This should be a protocol in Python 3.10+"""
constraint_group: Optional[str]
"""Identifies the group this item rule is a part of, subject to min/max upgrades per unit"""
display_string: str
"""Main item to count as the parent for min/max upgrades per unit purposes"""
@abc.abstractmethod
def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: ...
@abc.abstractmethod
def parent_items(self) -> Sequence[str]: ...
class ItemPresent(PresenceRule):
def __init__(self, item_name: str) -> None:
self.item_name = item_name
self.constraint_group = item_name
self.display_string = item_name
def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool:
return self.item_name in inventory
def parent_items(self) -> List[str]:
return [self.item_name]
class AnyOf(PresenceRule):
def __init__(self, group: Iterable[str], main_item: Optional[str] = None, display_string: Optional[str] = None) -> None:
self.group = set(group)
self.constraint_group = main_item
self.display_string = display_string or main_item or ' | '.join(group)
def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool:
return len(self.group.intersection(inventory)) > 0
def parent_items(self) -> List[str]:
return sorted(self.group)
class AllOf(PresenceRule):
def __init__(self, group: Iterable[str], main_item: Optional[str] = None) -> None:
self.group = set(group)
self.constraint_group = main_item
self.display_string = main_item or ' & '.join(group)
def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool:
return len(self.group.intersection(inventory)) == len(self.group)
def parent_items(self) -> List[str]:
return sorted(self.group)
class AnyOfGroupAndOneOtherItem(PresenceRule):
def __init__(self, group: Iterable[str], item_name: str) -> None:
self.group = set(group)
self.item_name = item_name
self.constraint_group = item_name
self.display_string = item_name
def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool:
return (len(self.group.intersection(inventory)) > 0) and self.item_name in inventory
def parent_items(self) -> List[str]:
return sorted(self.group) + [self.item_name]
class MorphlingOrItem(PresenceRule):
def __init__(self, item_name: str, has_parent: bool = True) -> None:
self.item_name = item_name
self.constraint_group = None # Keep morphs from counting towards the parent unit's upgrade count
self.display_string = f'{item_name} Morphs'
def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool:
return (options.enable_morphling.value != 0) or self.item_name in inventory
def parent_items(self) -> List[str]:
return [self.item_name]
class MorphlingOrAnyOf(PresenceRule):
def __init__(self, group: Iterable[str], display_string: str, main_item: Optional[str] = None) -> None:
self.group = set(group)
self.constraint_group = main_item
self.display_string = display_string
def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool:
return (options.enable_morphling.value != 0) or (len(self.group.intersection(inventory)) > 0)
def parent_items(self) -> List[str]:
return sorted(self.group)
parent_present: Dict[str, PresenceRule] = {
item_name: ItemPresent(item_name)
for item_name in item_tables.item_table
}
# Terran
parent_present[parent_names.DOMINION_TROOPER_WEAPONS] = AnyOf([
item_names.DOMINION_TROOPER_B2_HIGH_CAL_LMG,
item_names.DOMINION_TROOPER_CPO7_SALAMANDER_FLAMETHROWER,
item_names.DOMINION_TROOPER_HAILSTORM_LAUNCHER,
], main_item=item_names.DOMINION_TROOPER)
parent_present[parent_names.INFANTRY_UNITS] = AnyOf(item_groups.barracks_units, display_string='Terran Infantry')
parent_present[parent_names.INFANTRY_WEAPON_UNITS] = AnyOf(item_groups.barracks_wa_group, display_string='Terran Infantry')
parent_present[parent_names.ORBITAL_COMMAND_AND_PLANETARY] = AnyOfGroupAndOneOtherItem(
item_groups.orbital_command_abilities,
item_names.PLANETARY_FORTRESS,
)
parent_present[parent_names.SIEGE_TANK_AND_TRANSPORT] = AnyOfGroupAndOneOtherItem(
(item_names.MEDIVAC, item_names.HERCULES),
item_names.SIEGE_TANK,
)
parent_present[parent_names.SIEGE_TANK_AND_MEDIVAC] = AllOf((item_names.SIEGE_TANK, item_names.MEDIVAC), item_names.SIEGE_TANK)
parent_present[parent_names.SPIDER_MINE_SOURCE] = AnyOf(item_groups.spider_mine_sources, display_string='Spider Mines')
parent_present[parent_names.STARSHIP_UNITS] = AnyOf(item_groups.starport_units, display_string='Terran Starships')
parent_present[parent_names.STARSHIP_WEAPON_UNITS] = AnyOf(item_groups.starport_wa_group, display_string='Terran Starships')
parent_present[parent_names.VEHICLE_UNITS] = AnyOf(item_groups.factory_units, display_string='Terran Vehicles')
parent_present[parent_names.VEHICLE_WEAPON_UNITS] = AnyOf(item_groups.factory_wa_group, display_string='Terran Vehicles')
parent_present[parent_names.TERRAN_MERCENARIES] = AnyOf(item_groups.terran_mercenaries, display_string='Terran Mercenaries')
# Zerg
parent_present[parent_names.ANY_NYDUS_WORM] = AnyOf((item_names.NYDUS_WORM, item_names.ECHIDNA_WORM), item_names.NYDUS_WORM)
parent_present[parent_names.BANELING_SOURCE] = AnyOf(
(item_names.ZERGLING_BANELING_ASPECT, item_names.KERRIGAN_SPAWN_BANELINGS),
item_names.ZERGLING_BANELING_ASPECT,
)
parent_present[parent_names.INFESTED_UNITS] = AnyOf(item_groups.infterr_units, display_string='Infested')
parent_present[parent_names.INFESTED_FACTORY_OR_STARPORT] = AnyOf(
(item_names.INFESTED_DIAMONDBACK, item_names.INFESTED_SIEGE_TANK, item_names.INFESTED_LIBERATOR, item_names.INFESTED_BANSHEE, item_names.BULLFROG)
)
parent_present[parent_names.MORPH_SOURCE_AIR] = MorphlingOrAnyOf((item_names.MUTALISK, item_names.CORRUPTOR), "Mutalisk/Corruptor Morphs")
parent_present[parent_names.MORPH_SOURCE_ROACH] = MorphlingOrItem(item_names.ROACH)
parent_present[parent_names.MORPH_SOURCE_ZERGLING] = MorphlingOrItem(item_names.ZERGLING)
parent_present[parent_names.MORPH_SOURCE_HYDRALISK] = MorphlingOrItem(item_names.HYDRALISK)
parent_present[parent_names.MORPH_SOURCE_ULTRALISK] = MorphlingOrItem(item_names.ULTRALISK)
parent_present[parent_names.ZERG_UPROOTABLE_BUILDINGS] = AnyOf(
(item_names.SPINE_CRAWLER, item_names.SPORE_CRAWLER, item_names.INFESTED_MISSILE_TURRET, item_names.INFESTED_BUNKER),
)
parent_present[parent_names.ZERG_MELEE_ATTACKER] = AnyOf(item_groups.zerg_melee_wa, display_string='Zerg Ground')
parent_present[parent_names.ZERG_MISSILE_ATTACKER] = AnyOf(item_groups.zerg_ranged_wa, display_string='Zerg Ground')
parent_present[parent_names.ZERG_CARAPACE_UNIT] = AnyOf(item_groups.zerg_ground_units, display_string='Zerg Flyers')
parent_present[parent_names.ZERG_FLYING_UNIT] = AnyOf(item_groups.zerg_air_units, display_string='Zerg Flyers')
parent_present[parent_names.ZERG_MERCENARIES] = AnyOf(item_groups.zerg_mercenaries, display_string='Zerg Mercenaries')
parent_present[parent_names.ZERG_OUROBOUROS_CONDITION] = AnyOfGroupAndOneOtherItem(
(item_names.ZERGLING, item_names.ROACH, item_names.HYDRALISK, item_names.ABERRATION),
item_names.ECHIDNA_WORM
)
# Protoss
parent_present[parent_names.ARCHON_SOURCE] = AnyOf(
(item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.ASCENDANT_ARCHON_MERGE, item_names.DARK_TEMPLAR_ARCHON_MERGE),
main_item="Archon",
)
parent_present[parent_names.CARRIER_CLASS] = AnyOf(
(item_names.CARRIER, item_names.TRIREME, item_names.SKYLORD),
main_item=item_names.CARRIER,
)
parent_present[parent_names.CARRIER_OR_TRIREME] = AnyOf(
(item_names.CARRIER, item_names.TRIREME),
main_item=item_names.CARRIER,
)
parent_present[parent_names.DARK_ARCHON_SOURCE] = AnyOf(
(item_names.DARK_ARCHON, item_names.DARK_TEMPLAR_DARK_ARCHON_MELD),
main_item=item_names.DARK_ARCHON,
)
parent_present[parent_names.DARK_TEMPLAR_CLASS] = AnyOf(
(item_names.DARK_TEMPLAR, item_names.AVENGER, item_names.BLOOD_HUNTER),
main_item=item_names.DARK_TEMPLAR,
)
parent_present[parent_names.STORM_CASTER] = AnyOf(
(item_names.HIGH_TEMPLAR, item_names.SIGNIFIER),
main_item=item_names.HIGH_TEMPLAR,
)
parent_present[parent_names.IMMORTAL_OR_ANNIHILATOR] = AnyOf(
(item_names.IMMORTAL, item_names.ANNIHILATOR),
main_item=item_names.IMMORTAL,
)
parent_present[parent_names.PHOENIX_CLASS] = AnyOf(
(item_names.PHOENIX, item_names.MIRAGE, item_names.SKIRMISHER),
main_item=item_names.PHOENIX,
)
parent_present[parent_names.SENTRY_CLASS] = AnyOf(
(item_names.SENTRY, item_names.ENERGIZER, item_names.HAVOC),
main_item=item_names.SENTRY,
)
parent_present[parent_names.SENTRY_CLASS_OR_SHIELD_BATTERY] = AnyOf(
(item_names.SENTRY, item_names.ENERGIZER, item_names.HAVOC, item_names.SHIELD_BATTERY),
main_item=item_names.SENTRY,
)
parent_present[parent_names.STALKER_CLASS] = AnyOf(
(item_names.STALKER, item_names.SLAYER, item_names.INSTIGATOR),
main_item=item_names.STALKER,
)
parent_present[parent_names.SUPPLICANT_AND_ASCENDANT] = AllOf(
(item_names.SUPPLICANT, item_names.ASCENDANT),
main_item=item_names.ASCENDANT,
)
parent_present[parent_names.VOID_RAY_CLASS] = AnyOf(
(item_names.VOID_RAY, item_names.DESTROYER, item_names.PULSAR, item_names.DAWNBRINGER),
main_item=item_names.VOID_RAY,
)
parent_present[parent_names.ZEALOT_OR_SENTINEL_OR_CENTURION] = AnyOf(
(item_names.ZEALOT, item_names.SENTINEL, item_names.CENTURION),
main_item=item_names.ZEALOT,
)
parent_present[parent_names.SCOUT_CLASS] = AnyOf(
(item_names.SCOUT, item_names.OPPRESSOR, item_names.CALADRIUS, item_names.MISTWING),
main_item=item_names.SCOUT,
)
parent_present[parent_names.SCOUT_OR_OPPRESSOR_OR_MISTWING] = AnyOf(
(item_names.SCOUT, item_names.OPPRESSOR, item_names.MISTWING),
main_item=item_names.SCOUT,
)
parent_present[parent_names.PROTOSS_STATIC_DEFENSE] = AnyOf(
(item_names.NEXUS_OVERCHARGE, item_names.PHOTON_CANNON, item_names.KHAYDARIN_MONOLITH, item_names.SHIELD_BATTERY),
main_item=item_names.PHOTON_CANNON,
)
parent_present[parent_names.PROTOSS_ATTACKING_BUILDING] = AnyOf(
(item_names.NEXUS_OVERCHARGE, item_names.PHOTON_CANNON, item_names.KHAYDARIN_MONOLITH),
main_item=item_names.PHOTON_CANNON,
)
parent_id_to_children: Dict[str, Sequence[str]] = {}
"""Parent identifier to child items. Only contains parent rules with children."""
child_item_to_parent_items: Dict[str, Sequence[str]] = {}
"""Child item name to all parent items that can possibly affect its presence rule. Populated for all item names."""
parent_item_to_ids: Dict[str, Sequence[str]] = {}
"""Parent item to parent identifiers it affects. Populated for all items and parent IDs."""
parent_item_to_children: Dict[str, Sequence[str]] = {}
"""Parent item to child item names. Populated for all items and parent IDs."""
item_upgrade_groups: Dict[str, Sequence[str]] = {}
"""Mapping of upgradable item group -> child items. Only populated for groups with child items."""
# Note(mm): "All items" promise satisfied by the basic ItemPresent auto-generated rules
def _init() -> None:
for item_name, item_data in item_tables.item_table.items():
if item_data.parent is None:
continue
parent_id_to_children.setdefault(item_data.parent, []).append(item_name) # type: ignore
child_item_to_parent_items[item_name] = parent_present[item_data.parent].parent_items()
for parent_id, presence_func in parent_present.items():
for parent_item in presence_func.parent_items():
parent_item_to_ids.setdefault(parent_item, []).append(parent_id) # type: ignore
parent_item_to_children.setdefault(parent_item, []).extend(parent_id_to_children.get(parent_id, [])) # type: ignore
if presence_func.constraint_group is not None and parent_id_to_children.get(parent_id):
item_upgrade_groups.setdefault(presence_func.constraint_group, []).extend(parent_id_to_children[parent_id]) # type: ignore
_init()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
"""
Identifiers for complex item parent structures.
Defined separately from item_parents to avoid a circular import
item_names -> item_parent_names -> item_tables -> item_parents
"""
# Terran
DOMINION_TROOPER_WEAPONS = "Dominion Trooper Weapons"
INFANTRY_UNITS = "Infantry Units"
INFANTRY_WEAPON_UNITS = "Infantry Weapon Units"
ORBITAL_COMMAND_AND_PLANETARY = "Orbital Command Abilities + Planetary Fortress" # MULE | Scan | Supply Drop
SIEGE_TANK_AND_TRANSPORT = "Siege Tank + Transport"
SIEGE_TANK_AND_MEDIVAC = "Siege Tank + Medivac"
SPIDER_MINE_SOURCE = "Spider Mine Source"
STARSHIP_UNITS = "Starship Units"
STARSHIP_WEAPON_UNITS = "Starship Weapon Units"
VEHICLE_UNITS = "Vehicle Units"
VEHICLE_WEAPON_UNITS = "Vehicle Weapon Units"
TERRAN_MERCENARIES = "Terran Mercenaries"
# Zerg
ANY_NYDUS_WORM = "Any Nydus Worm"
BANELING_SOURCE = "Any Baneling Source" # Baneling aspect | Kerrigan Spawn Banelings
INFESTED_UNITS = "Infested Units"
INFESTED_FACTORY_OR_STARPORT = "Infested Factory or Starport"
MORPH_SOURCE_AIR = "Air Morph Source" # Morphling | Mutalisk | Corruptor
MORPH_SOURCE_ROACH = "Roach Morph Source" # Morphling | Roach
MORPH_SOURCE_ZERGLING = "Zergling Morph Source" # Morphling | Zergling
MORPH_SOURCE_HYDRALISK = "Hydralisk Morph Source" # Morphling | Hydralisk
MORPH_SOURCE_ULTRALISK = "Ultralisk Morph Source" # Morphling | Ultralisk
ZERG_UPROOTABLE_BUILDINGS = "Zerg Uprootable Buildings"
ZERG_MELEE_ATTACKER = "Zerg Melee Attacker"
ZERG_MISSILE_ATTACKER = "Zerg Missile Attacker"
ZERG_CARAPACE_UNIT = "Zerg Carapace Unit"
ZERG_FLYING_UNIT = "Zerg Flying Unit"
ZERG_MERCENARIES = "Zerg Mercenaries"
ZERG_OUROBOUROS_CONDITION = "Zerg Ourobouros Condition"
# Protoss
ARCHON_SOURCE = "Any Archon Source"
CARRIER_CLASS = "Carrier Class"
CARRIER_OR_TRIREME = "Carrier | Trireme"
DARK_ARCHON_SOURCE = "Dark Archon Source"
DARK_TEMPLAR_CLASS = "Dark Templar Class"
STORM_CASTER = "Storm Caster"
IMMORTAL_OR_ANNIHILATOR = "Immortal | Annihilator"
PHOENIX_CLASS = "Phoenix Class"
SENTRY_CLASS = "Sentry Class"
SENTRY_CLASS_OR_SHIELD_BATTERY = "Sentry Class | Shield Battery"
STALKER_CLASS = "Stalker Class"
SUPPLICANT_AND_ASCENDANT = "Supplicant + Ascendant"
VOID_RAY_CLASS = "Void Ray Class"
ZEALOT_OR_SENTINEL_OR_CENTURION = "Zealot | Sentinel | Centurion"
PROTOSS_STATIC_DEFENSE = "Protoss Static Defense"
PROTOSS_ATTACKING_BUILDING = "Protoss Attacking Structure"
SCOUT_CLASS = "Scout Class"
SCOUT_OR_OPPRESSOR_OR_MISTWING = "Scout | Oppressor | Mist Wing"

View File

@@ -0,0 +1,40 @@
"""
Location group definitions
"""
from typing import Dict, Set, Iterable
from .locations import DEFAULT_LOCATION_LIST, LocationData
from .mission_tables import lookup_name_to_mission, MissionFlag
def get_location_groups() -> Dict[str, Set[str]]:
result: Dict[str, Set[str]] = {}
locations: Iterable[LocationData] = DEFAULT_LOCATION_LIST
for location in locations:
if location.code is None:
# Beat events
continue
mission = lookup_name_to_mission.get(location.region)
if mission is None:
continue
if (MissionFlag.HasRaceSwap|MissionFlag.RaceSwap) & mission.flags:
# Location group including race-swapped variants of a location
agnostic_location_name = (
location.name
.replace(' (Terran)', '')
.replace(' (Protoss)', '')
.replace(' (Zerg)', '')
)
result.setdefault(agnostic_location_name, set()).add(location.name)
# Location group including all locations in all raceswaps
result.setdefault(mission.mission_name[:mission.mission_name.find(' (')], set()).add(location.name)
# Location group including all locations in a mission
result.setdefault(mission.mission_name, set()).add(location.name)
# Location group by location category
result.setdefault(location.type.name.title(), set()).add(location.name)
return result

14175
worlds/sc2/locations.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,194 @@
"""
Mission group aliases for use in yaml options.
"""
from typing import Dict, List, Set
from .mission_tables import SC2Mission, MissionFlag, SC2Campaign
class MissionGroupNames:
ALL_MISSIONS = "All Missions"
WOL_MISSIONS = "WoL Missions"
HOTS_MISSIONS = "HotS Missions"
LOTV_MISSIONS = "LotV Missions"
NCO_MISSIONS = "NCO Missions"
PROPHECY_MISSIONS = "Prophecy Missions"
PROLOGUE_MISSIONS = "Prologue Missions"
EPILOGUE_MISSIONS = "Epilogue Missions"
TERRAN_MISSIONS = "Terran Missions"
ZERG_MISSIONS = "Zerg Missions"
PROTOSS_MISSIONS = "Protoss Missions"
NOBUILD_MISSIONS = "No-Build Missions"
DEFENSE_MISSIONS = "Defense Missions"
AUTO_SCROLLER_MISSIONS = "Auto-Scroller Missions"
COUNTDOWN_MISSIONS = "Countdown Missions"
KERRIGAN_MISSIONS = "Kerrigan Missions"
VANILLA_SOA_MISSIONS = "Vanilla SOA Missions"
TERRAN_ALLY_MISSIONS = "Controllable Terran Ally Missions"
ZERG_ALLY_MISSIONS = "Controllable Zerg Ally Missions"
PROTOSS_ALLY_MISSIONS = "Controllable Protoss Ally Missions"
VS_TERRAN_MISSIONS = "Vs Terran Missions"
VS_ZERG_MISSIONS = "Vs Zerg Missions"
VS_PROTOSS_MISSIONS = "Vs Protoss Missions"
RACESWAP_MISSIONS = "Raceswap Missions"
# By planet
PLANET_MAR_SARA_MISSIONS = "Planet Mar Sara"
PLANET_CHAR_MISSIONS = "Planet Char"
PLANET_KORHAL_MISSIONS = "Planet Korhal"
PLANET_AIUR_MISSIONS = "Planet Aiur"
# By quest chain
WOL_MAR_SARA_MISSIONS = "WoL Mar Sara"
WOL_COLONIST_MISSIONS = "WoL Colonist"
WOL_ARTIFACT_MISSIONS = "WoL Artifact"
WOL_COVERT_MISSIONS = "WoL Covert"
WOL_REBELLION_MISSIONS = "WoL Rebellion"
WOL_CHAR_MISSIONS = "WoL Char"
HOTS_UMOJA_MISSIONS = "HotS Umoja"
HOTS_KALDIR_MISSIONS = "HotS Kaldir"
HOTS_CHAR_MISSIONS = "HotS Char"
HOTS_ZERUS_MISSIONS = "HotS Zerus"
HOTS_SKYGEIRR_MISSIONS = "HotS Skygeirr Station"
HOTS_DOMINION_SPACE_MISSIONS = "HotS Dominion Space"
HOTS_KORHAL_MISSIONS = "HotS Korhal"
LOTV_AIUR_MISSIONS = "LotV Aiur"
LOTV_KORHAL_MISSIONS = "LotV Korhal"
LOTV_SHAKURAS_MISSIONS = "LotV Shakuras"
LOTV_ULNAR_MISSIONS = "LotV Ulnar"
LOTV_PURIFIER_MISSIONS = "LotV Purifier"
LOTV_TALDARIM_MISSIONS = "LotV Tal'darim"
LOTV_MOEBIUS_MISSIONS = "LotV Moebius"
LOTV_RETURN_TO_AIUR_MISSIONS = "LotV Return to Aiur"
NCO_MISSION_PACK_1 = "NCO Mission Pack 1"
NCO_MISSION_PACK_2 = "NCO Mission Pack 2"
NCO_MISSION_PACK_3 = "NCO Mission Pack 3"
@classmethod
def get_all_group_names(cls) -> Set[str]:
return {
name
for identifier, name in cls.__dict__.items()
if not identifier.startswith("_") and not identifier.startswith("get_")
}
mission_groups: Dict[str, List[str]] = {}
mission_groups[MissionGroupNames.ALL_MISSIONS] = [mission.mission_name for mission in SC2Mission]
for group_name, campaign in (
(MissionGroupNames.WOL_MISSIONS, SC2Campaign.WOL),
(MissionGroupNames.HOTS_MISSIONS, SC2Campaign.HOTS),
(MissionGroupNames.LOTV_MISSIONS, SC2Campaign.LOTV),
(MissionGroupNames.NCO_MISSIONS, SC2Campaign.NCO),
(MissionGroupNames.PROPHECY_MISSIONS, SC2Campaign.PROPHECY),
(MissionGroupNames.PROLOGUE_MISSIONS, SC2Campaign.PROLOGUE),
(MissionGroupNames.EPILOGUE_MISSIONS, SC2Campaign.EPILOGUE),
):
mission_groups[group_name] = [mission.mission_name for mission in SC2Mission if mission.campaign == campaign]
for group_name, flags in (
(MissionGroupNames.TERRAN_MISSIONS, MissionFlag.Terran),
(MissionGroupNames.ZERG_MISSIONS, MissionFlag.Zerg),
(MissionGroupNames.PROTOSS_MISSIONS, MissionFlag.Protoss),
(MissionGroupNames.NOBUILD_MISSIONS, MissionFlag.NoBuild),
(MissionGroupNames.DEFENSE_MISSIONS, MissionFlag.Defense),
(MissionGroupNames.AUTO_SCROLLER_MISSIONS, MissionFlag.AutoScroller),
(MissionGroupNames.COUNTDOWN_MISSIONS, MissionFlag.Countdown),
(MissionGroupNames.KERRIGAN_MISSIONS, MissionFlag.Kerrigan),
(MissionGroupNames.VANILLA_SOA_MISSIONS, MissionFlag.VanillaSoa),
(MissionGroupNames.TERRAN_ALLY_MISSIONS, MissionFlag.AiTerranAlly),
(MissionGroupNames.ZERG_ALLY_MISSIONS, MissionFlag.AiZergAlly),
(MissionGroupNames.PROTOSS_ALLY_MISSIONS, MissionFlag.AiProtossAlly),
(MissionGroupNames.VS_TERRAN_MISSIONS, MissionFlag.VsTerran),
(MissionGroupNames.VS_ZERG_MISSIONS, MissionFlag.VsZerg),
(MissionGroupNames.VS_PROTOSS_MISSIONS, MissionFlag.VsProtoss),
(MissionGroupNames.RACESWAP_MISSIONS, MissionFlag.RaceSwap),
):
mission_groups[group_name] = [mission.mission_name for mission in SC2Mission if flags in mission.flags]
for group_name, campaign, chain_name in (
(MissionGroupNames.WOL_MAR_SARA_MISSIONS, SC2Campaign.WOL, "Mar Sara"),
(MissionGroupNames.WOL_COLONIST_MISSIONS, SC2Campaign.WOL, "Colonist"),
(MissionGroupNames.WOL_ARTIFACT_MISSIONS, SC2Campaign.WOL, "Artifact"),
(MissionGroupNames.WOL_COVERT_MISSIONS, SC2Campaign.WOL, "Covert"),
(MissionGroupNames.WOL_REBELLION_MISSIONS, SC2Campaign.WOL, "Rebellion"),
(MissionGroupNames.WOL_CHAR_MISSIONS, SC2Campaign.WOL, "Char"),
(MissionGroupNames.HOTS_UMOJA_MISSIONS, SC2Campaign.HOTS, "Umoja"),
(MissionGroupNames.HOTS_KALDIR_MISSIONS, SC2Campaign.HOTS, "Kaldir"),
(MissionGroupNames.HOTS_CHAR_MISSIONS, SC2Campaign.HOTS, "Char"),
(MissionGroupNames.HOTS_ZERUS_MISSIONS, SC2Campaign.HOTS, "Zerus"),
(MissionGroupNames.HOTS_SKYGEIRR_MISSIONS, SC2Campaign.HOTS, "Skygeirr Station"),
(MissionGroupNames.HOTS_DOMINION_SPACE_MISSIONS, SC2Campaign.HOTS, "Dominion Space"),
(MissionGroupNames.HOTS_KORHAL_MISSIONS, SC2Campaign.HOTS, "Korhal"),
(MissionGroupNames.LOTV_AIUR_MISSIONS, SC2Campaign.LOTV, "Aiur"),
(MissionGroupNames.LOTV_KORHAL_MISSIONS, SC2Campaign.LOTV, "Korhal"),
(MissionGroupNames.LOTV_SHAKURAS_MISSIONS, SC2Campaign.LOTV, "Shakuras"),
(MissionGroupNames.LOTV_ULNAR_MISSIONS, SC2Campaign.LOTV, "Ulnar"),
(MissionGroupNames.LOTV_PURIFIER_MISSIONS, SC2Campaign.LOTV, "Purifier"),
(MissionGroupNames.LOTV_TALDARIM_MISSIONS, SC2Campaign.LOTV, "Tal'darim"),
(MissionGroupNames.LOTV_MOEBIUS_MISSIONS, SC2Campaign.LOTV, "Moebius"),
(MissionGroupNames.LOTV_RETURN_TO_AIUR_MISSIONS, SC2Campaign.LOTV, "Return to Aiur"),
):
mission_groups[group_name] = [
mission.mission_name for mission in SC2Mission if mission.campaign == campaign and mission.area == chain_name
]
mission_groups[MissionGroupNames.NCO_MISSION_PACK_1] = [
SC2Mission.THE_ESCAPE.mission_name,
SC2Mission.SUDDEN_STRIKE.mission_name,
SC2Mission.ENEMY_INTELLIGENCE.mission_name,
]
mission_groups[MissionGroupNames.NCO_MISSION_PACK_2] = [
SC2Mission.TROUBLE_IN_PARADISE.mission_name,
SC2Mission.NIGHT_TERRORS.mission_name,
SC2Mission.FLASHPOINT.mission_name,
]
mission_groups[MissionGroupNames.NCO_MISSION_PACK_3] = [
SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name,
SC2Mission.DARK_SKIES.mission_name,
SC2Mission.END_GAME.mission_name,
]
mission_groups[MissionGroupNames.PLANET_MAR_SARA_MISSIONS] = [
SC2Mission.LIBERATION_DAY.mission_name,
SC2Mission.THE_OUTLAWS.mission_name,
SC2Mission.ZERO_HOUR.mission_name,
]
mission_groups[MissionGroupNames.PLANET_CHAR_MISSIONS] = [
SC2Mission.GATES_OF_HELL.mission_name,
SC2Mission.BELLY_OF_THE_BEAST.mission_name,
SC2Mission.SHATTER_THE_SKY.mission_name,
SC2Mission.ALL_IN.mission_name,
SC2Mission.DOMINATION.mission_name,
SC2Mission.FIRE_IN_THE_SKY.mission_name,
SC2Mission.OLD_SOLDIERS.mission_name,
]
mission_groups[MissionGroupNames.PLANET_KORHAL_MISSIONS] = [
SC2Mission.MEDIA_BLITZ.mission_name,
SC2Mission.PLANETFALL.mission_name,
SC2Mission.DEATH_FROM_ABOVE.mission_name,
SC2Mission.THE_RECKONING.mission_name,
SC2Mission.SKY_SHIELD.mission_name,
SC2Mission.BROTHERS_IN_ARMS.mission_name,
]
mission_groups[MissionGroupNames.PLANET_AIUR_MISSIONS] = [
SC2Mission.ECHOES_OF_THE_FUTURE.mission_name,
SC2Mission.FOR_AIUR.mission_name,
SC2Mission.THE_GROWING_SHADOW.mission_name,
SC2Mission.THE_SPEAR_OF_ADUN.mission_name,
SC2Mission.TEMPLAR_S_RETURN.mission_name,
SC2Mission.THE_HOST.mission_name,
SC2Mission.SALVATION.mission_name,
]
for mission in SC2Mission:
if mission.flags & MissionFlag.HasRaceSwap:
short_name = mission.get_short_name()
mission_groups[short_name] = [
mission_var.mission_name for mission_var in SC2Mission if short_name in mission_var.mission_name
]

View File

@@ -0,0 +1,66 @@
from typing import List, Dict, Any, Callable, TYPE_CHECKING
from BaseClasses import CollectionState
from ..mission_tables import SC2Mission, MissionFlag, get_goal_location
from .mission_pools import SC2MOGenMissionPools
if TYPE_CHECKING:
from .nodes import SC2MOGenMissionOrder, SC2MOGenMission
class SC2MissionOrder:
"""
Wrapper class for a generated mission order. Contains helper functions for getting data about generated missions.
"""
def __init__(self, mission_order_node: 'SC2MOGenMissionOrder', mission_pools: SC2MOGenMissionPools):
self.mission_order_node: 'SC2MOGenMissionOrder' = mission_order_node
"""Root node of the mission order structure."""
self.mission_pools: SC2MOGenMissionPools = mission_pools
"""Manager for missions in the mission order."""
def get_used_flags(self) -> Dict[MissionFlag, int]:
"""Returns a dictionary of all used flags and their appearance count within the mission order.
Flags that don't appear in the mission order also don't appear in this dictionary."""
return self.mission_pools.get_used_flags()
def get_used_missions(self) -> List[SC2Mission]:
"""Returns a list of all missions used in the mission order."""
return self.mission_pools.get_used_missions()
def get_mission_count(self) -> int:
"""Returns the amount of missions in the mission order."""
return sum(
len([mission for mission in layout.missions if not mission.option_empty])
for campaign in self.mission_order_node.campaigns for layout in campaign.layouts
)
def get_starting_missions(self) -> List[SC2Mission]:
"""Returns a list containing all the missions that are accessible without beating any other missions."""
return [
slot.mission
for campaign in self.mission_order_node.campaigns if campaign.is_always_unlocked()
for layout in campaign.layouts if layout.is_always_unlocked()
for slot in layout.missions if slot.is_always_unlocked() and not slot.option_empty
]
def get_completion_condition(self, player: int) -> Callable[[CollectionState], bool]:
"""Returns a lambda to determine whether a state has beaten the mission order's required campaigns."""
final_locations = [get_goal_location(mission.mission) for mission in self.get_final_missions()]
return lambda state, final_locations=final_locations: all(state.can_reach_location(loc, player) for loc in final_locations)
def get_final_mission_ids(self) -> List[int]:
"""Returns the IDs of all missions that are required to beat the mission order."""
return [mission.mission.id for mission in self.get_final_missions()]
def get_final_missions(self) -> List['SC2MOGenMission']:
"""Returns the slots of all missions that are required to beat the mission order."""
return self.mission_order_node.goal_missions
def get_items_to_lock(self) -> Dict[str, int]:
"""Returns a dict of item names and amounts that are required by Item entry rules."""
return self.mission_order_node.items_to_lock
def get_slot_data(self) -> List[Dict[str, Any]]:
"""Parses the mission order into a format usable for slot data."""
return self.mission_order_node.get_slot_data()

View File

@@ -0,0 +1,389 @@
from __future__ import annotations
from typing import Set, Callable, Dict, List, Union, TYPE_CHECKING, Any, NamedTuple
from abc import ABC, abstractmethod
from dataclasses import dataclass
from ..mission_tables import SC2Mission
from ..item.item_tables import item_table
from BaseClasses import CollectionState
if TYPE_CHECKING:
from .nodes import SC2MOGenMission
class EntryRule(ABC):
buffer_fulfilled: bool
buffer_depth: int
def __init__(self) -> None:
self.buffer_fulfilled = False
self.buffer_depth = -1
def is_always_fulfilled(self, in_region_creation: bool = False) -> bool:
return self.is_fulfilled(set(), in_region_creation)
@abstractmethod
def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_creation: bool) -> bool:
"""Used during region creation to ensure a beatable mission order.
`in_region_creation` should determine whether rules that cannot be handled during region creation (like Item rules)
report themselves as fulfilled or unfulfilled."""
return False
def is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_creation: bool) -> bool:
if len(beaten_missions) == 0:
# Special-cased to avoid the buffer
# This is used to determine starting missions
return self._is_fulfilled(beaten_missions, in_region_creation)
self.buffer_fulfilled = self.buffer_fulfilled or self._is_fulfilled(beaten_missions, in_region_creation)
return self.buffer_fulfilled
@abstractmethod
def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int:
"""Used during region creation to determine the minimum depth this entry rule can be cleared at."""
return -1
def get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int:
if not self.is_fulfilled(beaten_missions, in_region_creation = True):
return -1
if self.buffer_depth == -1:
self.buffer_depth = self._get_depth(beaten_missions)
return self.buffer_depth
@abstractmethod
def to_lambda(self, player: int) -> Callable[[CollectionState], bool]:
"""Passed to Archipelago for use during item placement."""
return lambda _: False
@abstractmethod
def to_slot_data(self) -> RuleData:
"""Used in the client to determine accessibility while playing and to populate tooltips."""
pass
@dataclass
class RuleData(ABC):
@abstractmethod
def tooltip(self, indents: int, missions: Dict[int, SC2Mission], done_color: str, not_done_color: str) -> str:
return ""
@abstractmethod
def shows_single_rule(self) -> bool:
return False
@abstractmethod
def is_accessible(
self, beaten_missions: Set[int], received_items: Dict[int, int]
) -> bool:
return False
class BeatMissionsEntryRule(EntryRule):
missions_to_beat: List[SC2MOGenMission]
visual_reqs: List[Union[str, SC2MOGenMission]]
def __init__(self, missions_to_beat: List[SC2MOGenMission], visual_reqs: List[Union[str, SC2MOGenMission]]):
super().__init__()
self.missions_to_beat = missions_to_beat
self.visual_reqs = visual_reqs
def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_check: bool) -> bool:
return beaten_missions.issuperset(self.missions_to_beat)
def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int:
return max(mission.min_depth for mission in self.missions_to_beat)
def to_lambda(self, player: int) -> Callable[[CollectionState], bool]:
return lambda state: state.has_all([mission.beat_item() for mission in self.missions_to_beat], player)
def to_slot_data(self) -> RuleData:
resolved_reqs: List[Union[str, int]] = [req if isinstance(req, str) else req.mission.id for req in self.visual_reqs]
mission_ids = [mission.mission.id for mission in self.missions_to_beat]
return BeatMissionsRuleData(
mission_ids,
resolved_reqs
)
@dataclass
class BeatMissionsRuleData(RuleData):
mission_ids: List[int]
visual_reqs: List[Union[str, int]]
def tooltip(self, indents: int, missions: Dict[int, SC2Mission], done_color: str, not_done_color: str) -> str:
indent = " ".join("" for _ in range(indents))
if len(self.visual_reqs) == 1:
req = self.visual_reqs[0]
return f"Beat {missions[req].mission_name if isinstance(req, int) else req}"
tooltip = f"Beat all of these:\n{indent}- "
reqs = [missions[req].mission_name if isinstance(req, int) else req for req in self.visual_reqs]
tooltip += f"\n{indent}- ".join(req for req in reqs)
return tooltip
def shows_single_rule(self) -> bool:
return len(self.visual_reqs) == 1
def is_accessible(
self, beaten_missions: Set[int], received_items: Dict[int, int]
) -> bool:
# Beat rules are accessible if all their missions are beaten and accessible
if not beaten_missions.issuperset(self.mission_ids):
return False
return True
class CountMissionsEntryRule(EntryRule):
missions_to_count: List[SC2MOGenMission]
target_amount: int
visual_reqs: List[Union[str, SC2MOGenMission]]
def __init__(self, missions_to_count: List[SC2MOGenMission], target_amount: int, visual_reqs: List[Union[str, SC2MOGenMission]]):
super().__init__()
self.missions_to_count = missions_to_count
if target_amount == -1 or target_amount > len(missions_to_count):
self.target_amount = len(missions_to_count)
else:
self.target_amount = target_amount
self.visual_reqs = visual_reqs
def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_check: bool) -> bool:
return self.target_amount <= len(beaten_missions.intersection(self.missions_to_count))
def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int:
sorted_missions = sorted(beaten_missions.intersection(self.missions_to_count), key = lambda mission: mission.min_depth)
mission_depth = max(mission.min_depth for mission in sorted_missions[:self.target_amount])
return max(mission_depth, self.target_amount - 1) # -1 because depth is zero-based but amount is one-based
def to_lambda(self, player: int) -> Callable[[CollectionState], bool]:
return lambda state: self.target_amount <= sum(state.has(mission.beat_item(), player) for mission in self.missions_to_count)
def to_slot_data(self) -> RuleData:
resolved_reqs: List[Union[str, int]] = [req if isinstance(req, str) else req.mission.id for req in self.visual_reqs]
mission_ids = [mission.mission.id for mission in sorted(self.missions_to_count, key = lambda mission: mission.min_depth)]
return CountMissionsRuleData(
mission_ids,
self.target_amount,
resolved_reqs
)
@dataclass
class CountMissionsRuleData(RuleData):
mission_ids: List[int]
amount: int
visual_reqs: List[Union[str, int]]
def tooltip(self, indents: int, missions: Dict[int, SC2Mission], done_color: str, not_done_color: str) -> str:
indent = " ".join("" for _ in range(indents))
if self.amount == len(self.mission_ids):
amount = "all"
else:
amount = str(self.amount)
if len(self.visual_reqs) == 1:
req = self.visual_reqs[0]
req_str = missions[req].mission_name if isinstance(req, int) else req
if self.amount == 1:
if type(req) == int:
return f"Beat {req_str}"
return f"Beat any mission from {req_str}"
return f"Beat {amount} missions from {req_str}"
if self.amount == 1:
tooltip = f"Beat any mission from:\n{indent}- "
else:
tooltip = f"Beat {amount} missions from:\n{indent}- "
reqs = [missions[req].mission_name if isinstance(req, int) else req for req in self.visual_reqs]
tooltip += f"\n{indent}- ".join(req for req in reqs)
return tooltip
def shows_single_rule(self) -> bool:
return len(self.visual_reqs) == 1
def is_accessible(
self, beaten_missions: Set[int], received_items: Dict[int, int]
) -> bool:
# Count rules are accessible if enough of their missions are beaten and accessible
return len([mission_id for mission_id in self.mission_ids if mission_id in beaten_missions]) >= self.amount
class SubRuleEntryRule(EntryRule):
rule_id: int
rules_to_check: List[EntryRule]
target_amount: int
min_depth: int
def __init__(self, rules_to_check: List[EntryRule], target_amount: int, rule_id: int):
super().__init__()
self.rule_id = rule_id
self.rules_to_check = rules_to_check
self.min_depth = -1
if target_amount == -1 or target_amount > len(rules_to_check):
self.target_amount = len(rules_to_check)
else:
self.target_amount = target_amount
def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_check: bool) -> bool:
return self.target_amount <= sum(rule.is_fulfilled(beaten_missions, in_region_check) for rule in self.rules_to_check)
def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int:
if len(self.rules_to_check) == 0:
return self.min_depth
# It should be guaranteed by is_fulfilled that enough rules have a valid depth because they are fulfilled
filtered_rules = [rule for rule in self.rules_to_check if rule.get_depth(beaten_missions) > -1]
sorted_rules = sorted(filtered_rules, key = lambda rule: rule.get_depth(beaten_missions))
required_depth = max(rule.get_depth(beaten_missions) for rule in sorted_rules[:self.target_amount])
return max(required_depth, self.min_depth)
def to_lambda(self, player: int) -> Callable[[CollectionState], bool]:
sub_lambdas = [rule.to_lambda(player) for rule in self.rules_to_check]
return lambda state, sub_lambdas=sub_lambdas: self.target_amount <= sum(sub_lambda(state) for sub_lambda in sub_lambdas)
def to_slot_data(self) -> SubRuleRuleData:
sub_rules = [rule.to_slot_data() for rule in self.rules_to_check]
return SubRuleRuleData(
self.rule_id,
sub_rules,
self.target_amount
)
@dataclass
class SubRuleRuleData(RuleData):
rule_id: int
sub_rules: List[RuleData]
amount: int
@staticmethod
def parse_from_dict(data: Dict[str, Any]) -> SubRuleRuleData:
amount = data["amount"]
rule_id = data["rule_id"]
sub_rules: List[RuleData] = []
for rule_data in data["sub_rules"]:
if "sub_rules" in rule_data:
rule: RuleData = SubRuleRuleData.parse_from_dict(rule_data)
elif "item_ids" in rule_data:
# Slot data converts Dict[int, int] to Dict[str, int] for some reason
item_ids = {int(item): item_amount for (item, item_amount) in rule_data["item_ids"].items()}
rule = ItemRuleData(
item_ids,
rule_data["visual_reqs"]
)
elif "amount" in rule_data:
rule = CountMissionsRuleData(
**{field: value for field, value in rule_data.items()}
)
else:
rule = BeatMissionsRuleData(
**{field: value for field, value in rule_data.items()}
)
sub_rules.append(rule)
rule = SubRuleRuleData(
rule_id,
sub_rules,
amount
)
return rule
@staticmethod
def empty() -> SubRuleRuleData:
return SubRuleRuleData(-1, [], 0)
def tooltip(self, indents: int, missions: Dict[int, SC2Mission], done_color: str, not_done_color: str) -> str:
indent = " ".join("" for _ in range(indents))
if self.amount == len(self.sub_rules):
if self.amount == 1:
return self.sub_rules[0].tooltip(indents, missions, done_color, not_done_color)
amount = "all"
elif self.amount == 1:
amount = "any"
else:
amount = str(self.amount)
tooltip = f"Fulfill {amount} of these conditions:\n{indent}- "
subrule_tooltips: List[str] = []
for rule in self.sub_rules:
sub_tooltip = rule.tooltip(indents + 4, missions, done_color, not_done_color)
if getattr(rule, "was_accessible", False):
subrule_tooltips.append(f"[color={done_color}]{sub_tooltip}[/color]")
else:
subrule_tooltips.append(f"[color={not_done_color}]{sub_tooltip}[/color]")
tooltip += f"\n{indent}- ".join(sub_tooltip for sub_tooltip in subrule_tooltips)
return tooltip
def shows_single_rule(self) -> bool:
return self.amount == len(self.sub_rules) == 1 and self.sub_rules[0].shows_single_rule()
def is_accessible(
self, beaten_missions: Set[int], received_items: Dict[int, int]
) -> bool:
# Sub-rule rules are accessible if enough of their child rules are accessible
accessible_count = 0
success = accessible_count >= self.amount
if self.amount > 0:
for rule in self.sub_rules:
if rule.is_accessible(beaten_missions, received_items):
rule.was_accessible = True
accessible_count += 1
if accessible_count >= self.amount:
success = True
break
else:
rule.was_accessible = False
return success
class MissionEntryRules(NamedTuple):
mission_rule: SubRuleRuleData
layout_rule: SubRuleRuleData
campaign_rule: SubRuleRuleData
class ItemEntryRule(EntryRule):
items_to_check: Dict[str, int]
def __init__(self, items_to_check: Dict[str, int]) -> None:
super().__init__()
self.items_to_check = items_to_check
def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_check: bool) -> bool:
# Region creation should assume items can be placed,
# but later uses (eg. starter missions) should respect that this locks a mission
return in_region_check
def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int:
# Depth 0 means this rule requires 0 prior beaten missions
return 0
def to_lambda(self, player: int) -> Callable[[CollectionState], bool]:
return lambda state: state.has_all_counts(self.items_to_check, player)
def to_slot_data(self) -> RuleData:
item_ids = {item_table[item].code: amount for (item, amount) in self.items_to_check.items()}
visual_reqs = [item if amount == 1 else str(amount) + "x " + item for (item, amount) in self.items_to_check.items()]
return ItemRuleData(
item_ids,
visual_reqs
)
@dataclass
class ItemRuleData(RuleData):
item_ids: Dict[int, int]
visual_reqs: List[str]
def tooltip(self, indents: int, missions: Dict[int, SC2Mission], done_color: str, not_done_color: str) -> str:
indent = " ".join("" for _ in range(indents))
if len(self.visual_reqs) == 1:
return f"Find {self.visual_reqs[0]}"
tooltip = f"Find all of these:\n{indent}- "
tooltip += f"\n{indent}- ".join(req for req in self.visual_reqs)
return tooltip
def shows_single_rule(self) -> bool:
return len(self.visual_reqs) == 1
def is_accessible(
self, beaten_missions: Set[int], received_items: Dict[int, int]
) -> bool:
return all(
item in received_items and received_items[item] >= amount
for (item, amount) in self.item_ids.items()
)

View File

@@ -0,0 +1,702 @@
"""
Contains the complex data manipulation functions for mission order generation and Archipelago region creation.
Incoming data is validated to match specifications in .options.py.
The functions here are called from ..regions.py.
"""
from typing import Set, Dict, Any, List, Tuple, Union, Optional, Callable, TYPE_CHECKING
import logging
from BaseClasses import Location, Region, Entrance
from ..mission_tables import SC2Mission, MissionFlag, lookup_name_to_mission, lookup_id_to_mission
from ..item.item_tables import named_layout_key_item_table, named_campaign_key_item_table
from ..item import item_names
from .nodes import MissionOrderNode, SC2MOGenMissionOrder, SC2MOGenCampaign, SC2MOGenLayout, SC2MOGenMission
from .entry_rules import EntryRule, SubRuleEntryRule, ItemEntryRule, CountMissionsEntryRule, BeatMissionsEntryRule
from .mission_pools import (
SC2MOGenMissionPools, Difficulty, modified_difficulty_thresholds, STANDARD_DIFFICULTY_FILL_ORDER
)
from .options import GENERIC_KEY_NAME, GENERIC_PROGRESSIVE_KEY_NAME
if TYPE_CHECKING:
from ..locations import LocationData
from .. import SC2World
def resolve_unlocks(mission_order: SC2MOGenMissionOrder):
"""Parses a mission order's entry rule dicts into entry rule objects."""
rolling_rule_id = 0
for campaign in mission_order.campaigns:
entry_rule = {
"rules": campaign.option_entry_rules,
"amount": -1
}
campaign.entry_rule = dict_to_entry_rule(mission_order, entry_rule, campaign, rolling_rule_id)
rolling_rule_id += 1
for layout in campaign.layouts:
entry_rule = {
"rules": layout.option_entry_rules,
"amount": -1
}
layout.entry_rule = dict_to_entry_rule(mission_order, entry_rule, layout, rolling_rule_id)
rolling_rule_id += 1
for mission in layout.missions:
entry_rule = {
"rules": mission.option_entry_rules,
"amount": -1
}
mission.entry_rule = dict_to_entry_rule(mission_order, entry_rule, mission, rolling_rule_id)
rolling_rule_id += 1
# Manually make a rule for prev missions
if len(mission.prev) > 0:
mission.entry_rule.target_amount += 1
mission.entry_rule.rules_to_check.append(CountMissionsEntryRule(mission.prev, 1, mission.prev))
def dict_to_entry_rule(mission_order: SC2MOGenMissionOrder, data: Dict[str, Any], start_node: MissionOrderNode, rule_id: int = -1) -> EntryRule:
"""Tries to create an entry rule object from an entry rule dict. The structure of these dicts is validated in .options.py."""
if "items" in data:
items: Dict[str, int] = data["items"]
has_generic_key = False
for (item, amount) in items.items():
if item.casefold() == GENERIC_KEY_NAME or item.casefold().startswith(GENERIC_PROGRESSIVE_KEY_NAME):
has_generic_key = True
continue # Don't try to lock the generic key
if item in mission_order.items_to_lock:
# Lock the greatest required amount of each item
mission_order.items_to_lock[item] = max(mission_order.items_to_lock[item], amount)
else:
mission_order.items_to_lock[item] = amount
rule = ItemEntryRule(items)
if has_generic_key:
mission_order.keys_to_resolve.setdefault(start_node, []).append(rule)
return rule
if "rules" in data:
rules = [dict_to_entry_rule(mission_order, subrule, start_node) for subrule in data["rules"]]
return SubRuleEntryRule(rules, data["amount"], rule_id)
if "scope" in data:
objects: List[Tuple[MissionOrderNode, str]] = []
for address in data["scope"]:
resolved = resolve_address(mission_order, address, start_node)
objects.extend((obj, address) for obj in resolved)
visual_reqs = [obj.get_visual_requirement(start_node) for (obj, _) in objects]
missions: List[SC2MOGenMission]
if "amount" in data:
missions = [mission for (obj, _) in objects for mission in obj.get_missions() if not mission.option_empty]
if len(missions) == 0:
raise ValueError(f"Count rule did not find any missions at scopes: {data['scope']}")
return CountMissionsEntryRule(missions, data["amount"], visual_reqs)
missions = []
for (obj, address) in objects:
obj.important_beat_event = True
exits = obj.get_exits()
if len(exits) == 0:
raise ValueError(
f"Address \"{address}\" found an unbeatable object. "
"This should mean the address contains \"..\" too often."
)
missions.extend(exits)
return BeatMissionsEntryRule(missions, visual_reqs)
raise ValueError(f"Invalid data for entry rule: {data}")
def resolve_address(mission_order: SC2MOGenMissionOrder, address: str, start_node: MissionOrderNode) -> List[MissionOrderNode]:
"""Tries to find a node in the mission order by following the given address."""
if address.startswith("../") or address == "..":
# Relative address, starts from searching object
cursor = start_node
else:
# Absolute address, starts from the top
cursor = mission_order
address_so_far = ""
for term in address.split("/"):
if len(address_so_far) > 0:
address_so_far += "/"
address_so_far += term
if term == "..":
cursor = cursor.get_parent(address_so_far, address)
else:
result = cursor.search(term)
if result is None:
raise ValueError(f"Address \"{address_so_far}\" (from \"{address}\") tried to find a child for a mission.")
if len(result) == 0:
raise ValueError(f"Address \"{address_so_far}\" (from \"{address}\") could not find a {cursor.child_type_name()}.")
if len(result) > 1:
# Layouts are allowed to end with multiple missions via an index function
if type(result[0]) == SC2MOGenMission and address_so_far == address:
return result
raise ValueError((f"Address \"{address_so_far}\" (from \"{address}\") found more than one {cursor.child_type_name()}."))
cursor = result[0]
if cursor == start_node:
raise ValueError(
f"Address \"{address_so_far}\" (from \"{address}\") returned to original object. "
"This is not allowed to avoid circular requirements."
)
return [cursor]
########################
def fill_depths(mission_order: SC2MOGenMissionOrder) -> None:
"""
Flood-fills the mission order by following its entry rules to determine the depth of all nodes.
This also ensures theoretical total accessibility of all nodes, but this is allowed to be violated by item placement and the accessibility setting.
"""
accessible_campaigns: Set[SC2MOGenCampaign] = {campaign for campaign in mission_order.campaigns if campaign.is_always_unlocked(in_region_creation=True)}
next_campaigns: Set[SC2MOGenCampaign] = set(mission_order.campaigns).difference(accessible_campaigns)
accessible_layouts: Set[SC2MOGenLayout] = {
layout
for campaign in accessible_campaigns for layout in campaign.layouts
if layout.is_always_unlocked(in_region_creation=True)
}
next_layouts: Set[SC2MOGenLayout] = {layout for campaign in accessible_campaigns for layout in campaign.layouts}.difference(accessible_layouts)
next_missions: Set[SC2MOGenMission] = {mission for layout in accessible_layouts for mission in layout.entrances}
beaten_missions: Set[SC2MOGenMission] = set()
# Sanity check: Can any missions be accessed?
if len(next_missions) == 0:
raise Exception("Mission order has no possibly accessible missions")
iterations = 0
while len(next_missions) > 0:
# Check for accessible missions
cur_missions: Set[SC2MOGenMission] = {
mission for mission in next_missions
if mission.is_unlocked(beaten_missions, in_region_creation=True)
}
if len(cur_missions) == 0:
raise Exception(f"Mission order ran out of accessible missions during iteration {iterations}")
next_missions.difference_update(cur_missions)
# Set the depth counters of all currently accessible missions
new_beaten_missions: Set[SC2MOGenMission] = set()
while len(cur_missions) > 0:
mission = cur_missions.pop()
new_beaten_missions.add(mission)
# If the beaten missions at depth X unlock a mission, said mission can be beaten at depth X+1
mission.min_depth = mission.entry_rule.get_depth(beaten_missions) + 1
new_next = [
next_mission for next_mission in mission.next if not (
next_mission in cur_missions
or next_mission in beaten_missions
or next_mission in new_beaten_missions
)
]
next_missions.update(new_next)
# Any campaigns/layouts/missions added after this point will be seen in the next iteration at the earliest
iterations += 1
beaten_missions.update(new_beaten_missions)
# Check for newly accessible campaigns & layouts
new_campaigns: Set[SC2MOGenCampaign] = set()
for campaign in next_campaigns:
if campaign.is_unlocked(beaten_missions, in_region_creation=True):
new_campaigns.add(campaign)
for campaign in new_campaigns:
accessible_campaigns.add(campaign)
next_layouts.update(campaign.layouts)
next_campaigns.remove(campaign)
for layout in campaign.layouts:
layout.entry_rule.min_depth = campaign.entry_rule.get_depth(beaten_missions)
new_layouts: Set[SC2MOGenLayout] = set()
for layout in next_layouts:
if layout.is_unlocked(beaten_missions, in_region_creation=True):
new_layouts.add(layout)
for layout in new_layouts:
accessible_layouts.add(layout)
next_missions.update(layout.entrances)
next_layouts.remove(layout)
for mission in layout.entrances:
mission.entry_rule.min_depth = layout.entry_rule.get_depth(beaten_missions)
# Make sure we didn't miss anything
assert len(accessible_campaigns) == len(mission_order.campaigns)
assert len(accessible_layouts) == sum(len(campaign.layouts) for campaign in mission_order.campaigns)
total_missions = sum(
len([mission for mission in layout.missions if not mission.option_empty])
for campaign in mission_order.campaigns for layout in campaign.layouts
)
assert len(beaten_missions) == total_missions, f'Can only access {len(beaten_missions)} missions out of {total_missions}'
# Fill campaign/layout depth values as min/max of their children
for campaign in mission_order.campaigns:
for layout in campaign.layouts:
depths = [mission.min_depth for mission in layout.missions if not mission.option_empty]
layout.min_depth = min(depths)
layout.max_depth = max(depths)
campaign.min_depth = min(layout.min_depth for layout in campaign.layouts)
campaign.max_depth = max(layout.max_depth for layout in campaign.layouts)
mission_order.max_depth = max(campaign.max_depth for campaign in mission_order.campaigns)
########################
def resolve_difficulties(mission_order: SC2MOGenMissionOrder) -> None:
"""Determines the concrete difficulty of all mission slots."""
for campaign in mission_order.campaigns:
for layout in campaign.layouts:
if layout.option_min_difficulty == Difficulty.RELATIVE:
min_diff = campaign.option_min_difficulty
if min_diff == Difficulty.RELATIVE:
min_depth = 0
else:
min_depth = campaign.min_depth
else:
min_diff = layout.option_min_difficulty
min_depth = layout.min_depth
if layout.option_max_difficulty == Difficulty.RELATIVE:
max_diff = campaign.option_max_difficulty
if max_diff == Difficulty.RELATIVE:
max_depth = mission_order.max_depth
else:
max_depth = campaign.max_depth
else:
max_diff = layout.option_max_difficulty
max_depth = layout.max_depth
depth_range = max_depth - min_depth
if depth_range == 0:
# This can happen if layout size is 1 or layout is all entrances
# Use minimum difficulty in this case
depth_range = 1
# If min/max aren't relative, assume the limits are meant to show up
layout_thresholds = modified_difficulty_thresholds(min_diff, max_diff)
thresholds = sorted(layout_thresholds.keys())
for mission in layout.missions:
if mission.option_empty:
continue
if len(mission.option_mission_pool) == 1:
mission_order.fixed_missions.append(mission)
continue
if mission.option_difficulty == Difficulty.RELATIVE:
mission_thresh = int((mission.min_depth - min_depth) * 100 / depth_range)
for i in range(len(thresholds)):
if thresholds[i] > mission_thresh:
mission.option_difficulty = layout_thresholds[thresholds[i - 1]]
break
mission.option_difficulty = layout_thresholds[thresholds[-1]]
mission_order.sorted_missions[mission.option_difficulty].append(mission)
########################
def fill_missions(
mission_order: SC2MOGenMissionOrder, mission_pools: SC2MOGenMissionPools,
world: 'SC2World', locked_missions: List[str], locations: Tuple['LocationData', ...], location_cache: List[Location]
) -> None:
"""Places missions in all non-empty mission slots. Also responsible for creating Archipelago regions & locations for placed missions."""
locations_per_region = get_locations_per_region(locations)
regions: List[Region] = [create_region(world, locations_per_region, location_cache, "Menu")]
locked_ids = [lookup_name_to_mission[mission].id for mission in locked_missions]
prefer_close_difficulty = world.options.difficulty_curve.value == world.options.difficulty_curve.option_standard
def set_mission_in_slot(slot: SC2MOGenMission, mission: SC2Mission):
slot.mission = mission
slot.region = create_region(world, locations_per_region, location_cache,
mission.mission_name, slot)
# Resolve slots with set mission names
for mission_slot in mission_order.fixed_missions:
mission_id = mission_slot.option_mission_pool.pop()
# Remove set mission from locked missions
locked_ids = [locked for locked in locked_ids if locked != mission_id]
mission = lookup_id_to_mission[mission_id]
if mission in mission_pools.get_used_missions():
raise ValueError(f"Mission slot at address \"{mission_slot.get_address_to_node()}\" tried to plando an already plando'd mission.")
mission_pools.pull_specific_mission(mission)
set_mission_in_slot(mission_slot, mission)
regions.append(mission_slot.region)
# Shuffle & sort all slots to pick from smallest to biggest pool with tie-breaks by difficulty (lowest to highest), then randomly
# Additionally sort goals by difficulty (highest to lowest) with random tie-breaks
sorted_goals: List[SC2MOGenMission] = []
for difficulty in sorted(mission_order.sorted_missions.keys()):
world.random.shuffle(mission_order.sorted_missions[difficulty])
sorted_goals.extend(mission for mission in mission_order.sorted_missions[difficulty] if mission in mission_order.goal_missions)
# Sort slots by difficulty, with difficulties sorted by fill order
# standard curve/close difficulty fills difficulties out->in, uneven fills easy->hard
if prefer_close_difficulty:
all_slots = [slot for diff in STANDARD_DIFFICULTY_FILL_ORDER for slot in mission_order.sorted_missions[diff]]
else:
all_slots = [slot for diff in sorted(mission_order.sorted_missions.keys()) for slot in mission_order.sorted_missions[diff]]
# Pick slots with a constrained mission pool first
all_slots.sort(key = lambda slot: len(slot.option_mission_pool.intersection(mission_pools.master_list)))
sorted_goals.reverse()
# Randomly assign locked missions to appropriate difficulties
slots_for_locked: Dict[int, List[SC2MOGenMission]] = {locked: [] for locked in locked_ids}
for mission_slot in all_slots:
allowed_locked = mission_slot.option_mission_pool.intersection(locked_ids)
for locked in allowed_locked:
slots_for_locked[locked].append(mission_slot)
for (locked, allowed_slots) in slots_for_locked.items():
locked_mission = lookup_id_to_mission[locked]
allowed_slots = [slot for slot in allowed_slots if slot in all_slots]
if len(allowed_slots) == 0:
logging.warning(f"SC2: Locked mission \"{locked_mission.mission_name}\" is not allowed in any remaining spot and will not be placed.")
continue
# This inherits the earlier sorting, but is now sorted again by relative difficulty
# The result is a sorting in order of nearest difficulty (preferring lower), then by smallest pool, then randomly
allowed_slots.sort(key = lambda slot: abs(slot.option_difficulty - locked_mission.pool + 1))
# The first slot should be most appropriate
mission_slot = allowed_slots[0]
mission_pools.pull_specific_mission(locked_mission)
set_mission_in_slot(mission_slot, locked_mission)
regions.append(mission_slot.region)
all_slots.remove(mission_slot)
if mission_slot in sorted_goals:
sorted_goals.remove(mission_slot)
# Pick goal missions first with stricter difficulty matching, and starting with harder goals
for goal_slot in sorted_goals:
try:
mission = mission_pools.pull_random_mission(world, goal_slot, prefer_close_difficulty=True)
set_mission_in_slot(goal_slot, mission)
regions.append(goal_slot.region)
all_slots.remove(goal_slot)
except IndexError:
raise IndexError(
f"Slot at address \"{goal_slot.get_address_to_node()}\" ran out of possible missions to place "
f"with {len(all_slots)} empty slots remaining."
)
# Pick random missions
remaining_count = len(all_slots)
for mission_slot in all_slots:
try:
mission = mission_pools.pull_random_mission(world, mission_slot, prefer_close_difficulty=prefer_close_difficulty)
set_mission_in_slot(mission_slot, mission)
regions.append(mission_slot.region)
remaining_count -= 1
except IndexError:
raise IndexError(
f"Slot at address \"{mission_slot.get_address_to_node()}\" ran out of possible missions to place "
f"with {remaining_count} empty slots remaining."
)
world.multiworld.regions += regions
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
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
location_cache.append(location)
return location
def create_minimal_logic_location(
world: 'SC2World', location_data: 'LocationData', region: Region, location_cache: List[Location], unit_count: int = 0,
) -> Location:
location = Location(world.player, location_data.name, location_data.code, region)
mission = lookup_name_to_mission.get(region.name)
if mission is None:
pass
elif location_data.hard_rule:
assert world.logic
unit_rule = world.logic.has_race_units(unit_count, mission.race)
location.access_rule = lambda state: unit_rule(state) and location_data.hard_rule(state)
else:
assert world.logic
location.access_rule = world.logic.has_race_units(unit_count, mission.race)
location_cache.append(location)
return location
def create_region(
world: 'SC2World',
locations_per_region: Dict[str, List['LocationData']],
location_cache: List[Location],
name: str,
slot: Optional[SC2MOGenMission] = None,
) -> Region:
MAX_UNIT_REQUIREMENT = 5
region = Region(name, world.player, world.multiworld)
from ..locations import LocationType
if slot is None:
target_victory_cache_locations = 0
else:
target_victory_cache_locations = slot.option_victory_cache
victory_cache_locations = 0
# If the first mission is a build mission,
# require a unit everywhere except one location in the easiest category
mission_needs_unit = False
unit_given = False
easiest_category = LocationType.MASTERY
if slot is not None and slot.min_depth == 0:
mission = lookup_name_to_mission.get(region.name)
if mission is not None and MissionFlag.NoBuild not in mission.flags:
mission_needs_unit = True
for location_data in locations_per_region.get(name, ()):
if location_data.type == LocationType.VICTORY:
pass
elif location_data.type < easiest_category:
easiest_category = location_data.type
if easiest_category >= LocationType.CHALLENGE:
easiest_category = LocationType.VICTORY
for location_data in locations_per_region.get(name, ()):
assert slot is not None
if location_data.type == LocationType.VICTORY_CACHE:
if victory_cache_locations >= target_victory_cache_locations:
continue
victory_cache_locations += 1
if world.options.required_tactics.value == world.options.required_tactics.option_any_units:
if mission_needs_unit and not unit_given and location_data.type == easiest_category:
# Ensure there is at least one no-logic location if the first mission is a build mission
location = create_minimal_logic_location(world, location_data, region, location_cache, 0)
unit_given = True
elif location_data.type == LocationType.MASTERY:
# Mastery locations always require max units regardless of position in the ramp
location = create_minimal_logic_location(world, location_data, region, location_cache, MAX_UNIT_REQUIREMENT)
else:
# Required number of units = mission depth; +1 if it's a starting build mission; +1 if it's a challenge location
location = create_minimal_logic_location(world, location_data, region, location_cache, min(
slot.min_depth + mission_needs_unit + (location_data.type == LocationType.CHALLENGE),
MAX_UNIT_REQUIREMENT
))
else:
location = create_location(world.player, location_data, region, location_cache)
region.locations.append(location)
return region
########################
def make_connections(mission_order: SC2MOGenMissionOrder, world: 'SC2World'):
"""Creates Archipelago entrances between missions and creates access rules for the generator from entry rule objects."""
names: Dict[str, int] = {}
player = world.player
for campaign in mission_order.campaigns:
for layout in campaign.layouts:
for mission in layout.missions:
if not mission.option_empty:
mission_rule = mission.entry_rule.to_lambda(player)
# Only layout entrances need to consider campaign & layout prerequisites
if mission.option_entrance:
campaign_rule = mission.parent().parent().entry_rule.to_lambda(player)
layout_rule = mission.parent().entry_rule.to_lambda(player)
unlock_rule = lambda state, campaign_rule=campaign_rule, layout_rule=layout_rule, mission_rule=mission_rule: \
campaign_rule(state) and layout_rule(state) and mission_rule(state)
else:
unlock_rule = mission_rule
# Individually connect to previous missions
for prev_mission in mission.prev:
connect(world, names, prev_mission.mission.mission_name, mission.mission.mission_name,
lambda state, unlock_rule=unlock_rule: unlock_rule(state))
# If there are no previous missions, connect to Menu instead
if len(mission.prev) == 0:
connect(world, names, "Menu", mission.mission.mission_name,
lambda state, unlock_rule=unlock_rule: unlock_rule(state))
def connect(world: 'SC2World', used_names: Dict[str, int], source: str, target: str,
rule: Optional[Callable] = None):
source_region = world.get_region(source)
target_region = world.get_region(target)
if target not in used_names:
used_names[target] = 1
name = target
else:
used_names[target] += 1
name = target + (' ' * used_names[target])
connection = Entrance(world.player, name, source_region)
if rule:
connection.access_rule = rule
source_region.exits.append(connection)
connection.connect(target_region)
########################
def resolve_generic_keys(mission_order: SC2MOGenMissionOrder) -> None:
"""
Replaces placeholder keys in Item entry rules with their concrete counterparts.
Specifically this handles placing named keys into missions and vanilla campaigns/layouts,
and assigning correct progression tracks to progressive keys.
"""
layout_numbered_keys = 1
campaign_numbered_keys = 1
progression_tracks: Dict[int, List[Tuple[MissionOrderNode, ItemEntryRule]]] = {}
for (node, item_rules) in mission_order.keys_to_resolve.items():
key_name = node.get_key_name()
# Generic keys in mission slots should always resolve to an existing key
# Layouts and campaigns may need to be switched for numbered keys
if isinstance(node, SC2MOGenLayout) and key_name not in named_layout_key_item_table:
key_name = item_names._TEMPLATE_NUMBERED_LAYOUT_KEY.format(layout_numbered_keys)
layout_numbered_keys += 1
elif isinstance(node, SC2MOGenCampaign) and key_name not in named_campaign_key_item_table:
key_name = item_names._TEMPLATE_NUMBERED_CAMPAIGN_KEY.format(campaign_numbered_keys)
campaign_numbered_keys += 1
for item_rule in item_rules:
# Swap regular generic key names for the node's proper key name
item_rule.items_to_check = {
key_name if item_name.casefold() == GENERIC_KEY_NAME else item_name: amount
for (item_name, amount) in item_rule.items_to_check.items()
}
# Only lock the key if it was actually placed in this rule
if key_name in item_rule.items_to_check:
mission_order.items_to_lock[key_name] = max(item_rule.items_to_check[key_name], mission_order.items_to_lock.get(key_name, 0))
# Sort progressive keys by their given track
for (item_name, amount) in item_rule.items_to_check.items():
if item_name.casefold() == GENERIC_PROGRESSIVE_KEY_NAME:
progression_tracks.setdefault(amount, []).append((node, item_rule))
elif item_name.casefold().startswith(GENERIC_PROGRESSIVE_KEY_NAME):
track_string = item_name.split()[-1]
try:
track = int(track_string)
progression_tracks.setdefault(track, []).append((node, item_rule))
except ValueError:
raise ValueError(
f"Progression track \"{track_string}\" for progressive key \"{item_name}: {amount}\" is not a valid number. "
"Valid formats are:\n"
f"- {GENERIC_PROGRESSIVE_KEY_NAME.title()}: X\n"
f"- {GENERIC_PROGRESSIVE_KEY_NAME.title()} X: 1"
)
def find_progressive_keys(item_rule: ItemEntryRule, track_to_find: int) -> List[str]:
return [
item_name for (item_name, amount) in item_rule.items_to_check.items()
if (item_name.casefold() == GENERIC_PROGRESSIVE_KEY_NAME and amount == track_to_find) or (
item_name.casefold().startswith(GENERIC_PROGRESSIVE_KEY_NAME) and
item_name.split()[-1] == str(track_to_find)
)
]
def replace_progressive_keys(item_rule: ItemEntryRule, track_to_replace: int, new_key_name: str, new_key_amount: int):
keys_to_replace = find_progressive_keys(item_rule, track_to_replace)
new_items_to_check: Dict[str, int] = {}
for (item_name, amount) in item_rule.items_to_check.items():
if item_name in keys_to_replace:
new_items_to_check[new_key_name] = new_key_amount
else:
new_items_to_check[item_name] = amount
item_rule.items_to_check = new_items_to_check
# Change progressive keys to be unique for missions and layouts that request it
want_unique: Dict[MissionOrderNode, List[Tuple[MissionOrderNode, ItemEntryRule]]] = {}
empty_tracks: List[int] = []
for track in progression_tracks:
# Sort keys to change by layout
new_unique_tracks: Dict[MissionOrderNode, List[Tuple[MissionOrderNode, ItemEntryRule]]] = {}
for (node, item_rule) in progression_tracks[track]:
if isinstance(node, SC2MOGenMission):
# Unique tracks for layouts take priority over campaigns
if node.parent().option_unique_progression_track == track:
new_unique_tracks.setdefault(node.parent(), []).append((node, item_rule))
elif node.parent().parent().option_unique_progression_track == track:
new_unique_tracks.setdefault(node.parent().parent(), []).append((node, item_rule))
elif isinstance(node, SC2MOGenLayout) and node.parent().option_unique_progression_track == track:
new_unique_tracks.setdefault(node.parent(), []).append((node, item_rule))
# Remove found keys from their original progression track
for (container_node, rule_list) in new_unique_tracks.items():
for node_and_rule in rule_list:
progression_tracks[track].remove(node_and_rule)
want_unique.setdefault(container_node, []).extend(rule_list)
if len(progression_tracks[track]) == 0:
empty_tracks.append(track)
for track in empty_tracks:
progression_tracks.pop(track)
# Make sure all tracks that can't have keys have been taken care of
invalid_tracks: List[int] = [track for track in progression_tracks if track < 1 or track > len(SC2Mission)]
if len(invalid_tracks) > 0:
affected_key_list: Dict[MissionOrderNode, List[str]] = {}
for track in invalid_tracks:
for (node, item_rule) in progression_tracks[track]:
affected_key_list.setdefault(node, []).extend(
f"{key}: {item_rule.items_to_check[key]}" for key in find_progressive_keys(item_rule, track)
)
affected_key_list_string = "\n- " + "\n- ".join(
f"{node.get_address_to_node()}: {affected_keys}"
for (node, affected_keys) in affected_key_list.items()
)
raise ValueError(
"Some item rules contain progressive keys with invalid tracks:" +
affected_key_list_string +
f"\nPossible solutions are changing the tracks of affected keys to be in the range from 1 to {len(SC2Mission)}, "
"or changing the unique_progression_track of containing campaigns/layouts to match the invalid tracks."
)
# Assign new free progression tracks to nodes in definition order
next_free = 1
nodes_to_assign = list(want_unique.keys())
while len(want_unique) > 0:
while next_free in progression_tracks:
next_free += 1
container_node = nodes_to_assign.pop(0)
progression_tracks[next_free] = want_unique.pop(container_node)
# Replace the affected keys in nodes with their correct counterparts
key_name = f"{GENERIC_PROGRESSIVE_KEY_NAME} {next_free}"
for (node, item_rule) in progression_tracks[next_free]:
# It's guaranteed by the sorting above that the container is either a layout or a campaign
replace_progressive_keys(item_rule, container_node.option_unique_progression_track, key_name, 1)
# Give progressive keys a more fitting name if there's only one track and they all apply to the same type of node
progressive_flavor_name: Union[str, None] = None
if len(progression_tracks) == 1:
if all(isinstance(node, SC2MOGenLayout) for rule_list in progression_tracks.values() for (node, _) in rule_list):
progressive_flavor_name = item_names.PROGRESSIVE_QUESTLINE_KEY
elif all(isinstance(node, SC2MOGenMission) for rule_list in progression_tracks.values() for (node, _) in rule_list):
progressive_flavor_name = item_names.PROGRESSIVE_MISSION_KEY
for (track, rule_list) in progression_tracks.items():
key_name = item_names._TEMPLATE_PROGRESSIVE_KEY.format(track) if progressive_flavor_name is None else progressive_flavor_name
# Determine order in which the rules should unlock
ordered_item_rules: List[List[ItemEntryRule]] = []
if not any(isinstance(node, SC2MOGenMission) for (node, _) in rule_list):
# No rule on this track belongs to a mission, so the rules can be kept in definition order
ordered_item_rules = [[item_rule] for (_, item_rule) in rule_list]
else:
# At least one rule belongs to a mission
# Sort rules by the depth of their nodes, ties get the same amount of keys
depth_to_rules: Dict[int, List[ItemEntryRule]] = {}
for (node, item_rule) in rule_list:
depth_to_rules.setdefault(node.get_min_depth(), []).append(item_rule)
ordered_item_rules = [depth_to_rules[depth] for depth in sorted(depth_to_rules.keys())]
# Assign correct progressive keys to each rule
for (position, item_rules) in enumerate(ordered_item_rules):
for item_rule in item_rules:
keys_to_replace = [
item_name for (item_name, amount) in item_rule.items_to_check.items()
if (item_name.casefold() == GENERIC_PROGRESSIVE_KEY_NAME and amount == track) or (
item_name.casefold().startswith(GENERIC_PROGRESSIVE_KEY_NAME) and
item_name.split()[-1] == str(track)
)
]
new_items_to_check: Dict[str, int] = {}
for (item_name, amount) in item_rule.items_to_check.items():
if item_name in keys_to_replace:
new_items_to_check[key_name] = position + 1
else:
new_items_to_check[item_name] = amount
item_rule.items_to_check = new_items_to_check
mission_order.items_to_lock[key_name] = len(ordered_item_rules)

View File

@@ -0,0 +1,620 @@
from __future__ import annotations
from typing import List, Callable, Set, Tuple, Union, TYPE_CHECKING, Dict, Any
import math
from abc import ABC, abstractmethod
if TYPE_CHECKING:
from .nodes import SC2MOGenMission
class LayoutType(ABC):
size: int
index_functions: List[str] = []
"""Names of available functions for mission indices. For list member `"my_fn"`, function should be called `idx_my_fn`."""
def __init__(self, size: int):
self.size = size
def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]:
"""Get type-specific options from the provided dict. Should return unused values."""
return options
@abstractmethod
def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]:
"""Use the provided `Callable` to create a one-dimensional list of mission slots and set up initial settings and connections.
This should include at least one entrance and exit."""
return []
def final_setup(self, missions: List[SC2MOGenMission]):
"""Called after user changes to the layout are applied to make any final checks and changes.
Implementers should make changes with caution, since it runs after a user's explicit commands are implemented."""
return
def parse_index(self, term: str) -> Union[Set[int], None]:
"""From the given term, determine a list of desired target indices. The term is guaranteed to not be "entrances", "exits", or "all".
If the term cannot be parsed, either raise an exception or return `None`."""
return self.parse_index_as_function(term)
def parse_index_as_function(self, term: str) -> Union[Set[int], None]:
"""Helper function to interpret the term as a function call on the layout type, if it is declared in `self.index_functions`.
Returns the function's return value if `term` is a valid function call, `None` otherwise."""
left = term.find('(')
right = term.find(')')
if left == -1 and right == -1:
# Assume no args are desired
fn_name = term.strip()
fn_args = []
elif left == -1 or right == -1:
return None
else:
fn_name = term[:left].strip()
fn_args_str = term[left + 1:right]
fn_args = [arg.strip() for arg in fn_args_str.split(',')]
if fn_name in self.index_functions:
try:
return getattr(self, "idx_" + fn_name)(*fn_args)
except:
return None
else:
return None
@abstractmethod
def get_visual_layout(self) -> List[List[int]]:
"""Organize the mission slots into a list of columns from left to right and top to bottom.
The list should contain indices into the list created by `make_slots`. Intentionally empty spots should contain -1.
The resulting 2D list should be rectangular."""
pass
class Column(LayoutType):
"""Linear layout. Default entrance is index 0 at the top, default exit is index `size - 1` at the bottom."""
# 0
# 1
# 2
def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]:
missions = [mission_factory() for _ in range(self.size)]
missions[0].option_entrance = True
missions[-1].option_exit = True
for i in range(self.size - 1):
missions[i].next.append(missions[i + 1])
return missions
def get_visual_layout(self) -> List[List[int]]:
return [list(range(self.size))]
class Grid(LayoutType):
"""Rectangular grid. Default entrance is index 0 in the top left, default exit is index `size - 1` in the bottom right."""
width: int
height: int
num_corners_to_remove: int
two_start_positions: bool
index_functions = [
"point", "rect"
]
# 0 1 2
# 3 4 5
# 6 7 8
def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]:
self.two_start_positions = options.pop("two_start_positions", False) and self.size >= 2
if self.two_start_positions:
self.size += 1
width: int = options.pop("width", 0)
if width < 1:
self.width, self.height, self.num_corners_to_remove = Grid.get_grid_dimensions(self.size)
else:
self.width = width
self.height = math.ceil(self.size / self.width)
self.num_corners_to_remove = self.height * width - self.size
return options
@staticmethod
def get_factors(number: int) -> Tuple[int, int]:
"""
Simple factorization into pairs of numbers (x, y) using a sieve method.
Returns the factorization that is most square, i.e. where x + y is minimized.
Factor order is such that x <= y.
"""
assert number > 0
for divisor in range(math.floor(math.sqrt(number)), 1, -1):
quotient = number // divisor
if quotient * divisor == number:
return divisor, quotient
return 1, number
@staticmethod
def get_grid_dimensions(size: int) -> Tuple[int, int, int]:
"""
Get the dimensions of a grid mission order from the number of missions, int the format (x, y, error).
* Error will always be 0, 1, or 2, so the missions can be removed from the corners that aren't the start or end.
* Dimensions are chosen such that x <= y, as buttons in the UI are wider than they are tall.
* Dimensions are chosen to be maximally square. That is, x + y + error is minimized.
* If multiple options of the same rating are possible, the one with the larger error is chosen,
as it will appear more square. Compare 3x11 to 5x7-2 for an example of this.
"""
dimension_candidates: List[Tuple[int, int, int]] = [(*Grid.get_factors(size + x), x) for x in (2, 1, 0)]
best_dimension = min(dimension_candidates, key=sum)
return best_dimension
@staticmethod
def manhattan_distance(point1: Tuple[int, int], point2: Tuple[int, int]) -> int:
return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1])
@staticmethod
def euclidean_distance_squared(point1: Tuple[int, int], point2: Tuple[int, int]) -> int:
return (point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2
@staticmethod
def euclidean_distance(point1: Tuple[int, int], point2: Tuple[int, int]) -> float:
return math.sqrt(Grid.euclidean_distance_squared(point1, point2))
def get_grid_coordinates(self, idx: int) -> Tuple[int, int]:
return (idx % self.width), (idx // self.width)
def get_grid_index(self, x: int, y: int) -> int:
return y * self.width + x
def is_valid_coordinates(self, x: int, y: int) -> bool:
return (
0 <= x < self.width and
0 <= y < self.height
)
def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]:
missions = [mission_factory() for _ in range(self.width * self.height)]
if self.two_start_positions:
missions[0].option_empty = True
missions[1].option_entrance = True
missions[self.get_grid_index(0, 1)].option_entrance = True
else:
missions[0].option_entrance = True
missions[-1].option_exit = True
for x in range(self.width):
left = x - 1
right = x + 1
for y in range(self.height):
up = y - 1
down = y + 1
idx = self.get_grid_index(x, y)
neighbours = [
self.get_grid_index(nb_x, nb_y)
for (nb_x, nb_y) in [(left, y), (right, y), (x, up), (x, down)]
if self.is_valid_coordinates(nb_x, nb_y)
]
missions[idx].next = [missions[nb] for nb in neighbours]
# Empty corners
top_corners = math.floor(self.num_corners_to_remove / 2)
bottom_corners = math.ceil(self.num_corners_to_remove / 2)
# Bottom left corners
y = self.height - 1
x = 0
leading_x = 0
placed = 0
while placed < bottom_corners:
if x == -1 or y == 0:
leading_x += 1
x = leading_x
y = self.height - 1
missions[self.get_grid_index(x, y)].option_empty = True
placed += 1
x -= 1
y -= 1
# Top right corners
y = 0
x = self.width - 1
leading_x = self.width - 1
placed = 0
while placed < top_corners:
if x == self.width or y == self.height - 1:
leading_x -= 1
x = leading_x
y = 0
missions[self.get_grid_index(x, y)].option_empty = True
placed += 1
x += 1
y += 1
return missions
def get_visual_layout(self) -> List[List[int]]:
columns = [
[self.get_grid_index(x, y) for y in range(self.height)]
for x in range(self.width)
]
return columns
def idx_point(self, x: str, y: str) -> Union[Set[int], None]:
try:
x = int(x)
y = int(y)
except:
return None
if self.is_valid_coordinates(x, y):
return {self.get_grid_index(x, y)}
return None
def idx_rect(self, x: str, y: str, width: str, height: str) -> Union[Set[int], None]:
try:
x = int(x)
y = int(y)
width = int(width)
height = int(height)
except:
return None
indices = {
self.get_grid_index(pt_x, pt_y)
for pt_y in range(y, y + height)
for pt_x in range(x, x + width)
if self.is_valid_coordinates(pt_x, pt_y)
}
return indices
class Canvas(Grid):
"""Rectangular grid that determines size and filled slots based on special canvas option."""
canvas: List[str]
groups: Dict[str, List[int]]
jump_distance_orthogonal: int
jump_distance_diagonal: int
jumps_orthogonal = [(-1, 0), (0, 1), (1, 0), (0, -1)]
jumps_diagonal = [(-1, -1), (-1, 1), (1, 1), (1, -1)]
index_functions = Grid.index_functions + ["group"]
def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]:
self.width = options.pop("width") # Should be guaranteed by the option parser
self.height = math.ceil(self.size / self.width)
self.num_corners_to_remove = 0
self.two_start_positions = False
self.jump_distance_orthogonal = max(options.pop("jump_distance_orthogonal", 1), 1)
self.jump_distance_diagonal = max(options.pop("jump_distance_diagonal", 1), 0)
if "canvas" not in options:
raise KeyError("Canvas layout is missing required canvas option. Either create it or change type to Grid.")
self.canvas = options.pop("canvas")
# Pad short lines with spaces
longest_line = max(len(line) for line in self.canvas)
for idx in range(len(self.canvas)):
padding = ' ' * (longest_line - len(self.canvas[idx]))
self.canvas[idx] += padding
self.groups = {}
for (line_idx, line) in enumerate(self.canvas):
for (char_idx, char) in enumerate(line):
self.groups.setdefault(char, []).append(self.get_grid_index(char_idx, line_idx))
return options
def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]:
missions = super().make_slots(mission_factory)
missions[0].option_entrance = False
missions[-1].option_exit = False
# Canvas spaces become empty slots
for idx in self.groups.get(" ", []):
missions[idx].option_empty = True
# Raycast into jump directions to find nearest empty space
def jump(point: Tuple[int, int], direction: Tuple[int, int], distance: int) -> Tuple[int, int]:
return (
point[0] + direction[0] * distance,
point[1] + direction[1] * distance
)
def raycast(point: Tuple[int, int], direction: Tuple[int, int], max_distance: int) -> Union[Tuple[int, SC2MOGenMission], None]:
for distance in range(1, max_distance + 1):
target = jump(point, direction, distance)
if self.is_valid_coordinates(*target):
target_mission = missions[self.get_grid_index(*target)]
if not target_mission.option_empty:
return (distance, target_mission)
else:
# Out of bounds
return None
return None
for (idx, mission) in enumerate(missions):
if mission.option_empty:
continue
point = self.get_grid_coordinates(idx)
if self.jump_distance_orthogonal > 1:
for direction in Canvas.jumps_orthogonal:
target = raycast(point, direction, self.jump_distance_orthogonal)
if target is not None:
(distance, target_mission) = target
if distance > 1:
# Distance 1 orthogonal jumps already come from the base grid
mission.next.append(target[1])
if self.jump_distance_diagonal > 0:
for direction in Canvas.jumps_diagonal:
target = raycast(point, direction, self.jump_distance_diagonal)
if target is not None:
(distance, target_mission) = target
if distance == 1:
# Keep distance 1 diagonal slots only if the orthogonal neighbours are empty
x_neighbour = jump(point, (direction[0], 0), 1)
y_neighbour = jump(point, (0, direction[1]), 1)
if (
missions[self.get_grid_index(*x_neighbour)].option_empty and
missions[self.get_grid_index(*y_neighbour)].option_empty
):
mission.next.append(target_mission)
else:
mission.next.append(target_mission)
return missions
def final_setup(self, missions: List[SC2MOGenMission]):
# Pick missions near the original start and end to set as default entrance/exit
# if the user didn't set one themselves
def distance_lambda(point: Tuple[int, int]) -> Callable[[Tuple[int, SC2MOGenMission]], int]:
return lambda idx_mission: Grid.euclidean_distance_squared(self.get_grid_coordinates(idx_mission[0]), point)
if not any(mission.option_entrance for mission in missions):
top_left = self.get_grid_coordinates(0)
closest_to_top_left = sorted(
((idx, mission) for (idx, mission) in enumerate(missions) if not mission.option_empty),
key = distance_lambda(top_left)
)
closest_to_top_left[0][1].option_entrance = True
if not any(mission.option_exit for mission in missions):
bottom_right = self.get_grid_coordinates(len(missions) - 1)
closest_to_bottom_right = sorted(
((idx, mission) for (idx, mission) in enumerate(missions) if not mission.option_empty),
key = distance_lambda(bottom_right)
)
closest_to_bottom_right[0][1].option_exit = True
def idx_group(self, group: str) -> Union[Set[int], None]:
if group not in self.groups:
return None
return set(self.groups[group])
class Hopscotch(LayoutType):
"""Alternating between one and two available missions.
Default entrance is index 0 in the top left, default exit is index `size - 1` in the bottom right."""
width: int
spacer: int
two_start_positions: bool
index_functions = [
"top", "bottom", "middle", "corner"
]
# 0 2
# 1 3 5
# 4 6
# 7
def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]:
self.two_start_positions = options.pop("two_start_positions", False) and self.size >= 2
if self.two_start_positions:
self.size += 1
width: int = options.pop("width", 7)
self.width = max(width, 4)
spacer: int = options.pop("spacer", 2)
self.spacer = max(spacer, 1)
return options
def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]:
slots = [mission_factory() for _ in range(self.size)]
if self.two_start_positions:
slots[0].option_empty = True
slots[1].option_entrance = True
slots[2].option_entrance = True
else:
slots[0].option_entrance = True
slots[-1].option_exit = True
cycle = 0
for idx in range(self.size):
if cycle == 0:
indices = [idx + 1, idx + 2]
cycle = 2
elif cycle == 1:
indices = [idx + 1]
cycle -= 1
else:
indices = [idx + 2]
cycle -= 1
for next_idx in indices:
if next_idx < self.size:
slots[idx].next.append(slots[next_idx])
return slots
@staticmethod
def space_at_column(idx: int) -> List[int]:
# -1 0 1 2 3 4 5
amount = idx - 1
if amount > 0:
return [-1 for _ in range(amount)]
else:
return []
def get_visual_layout(self) -> List[List[int]]:
# size offset by 1 to account for first column of two slots
cols: List[List[int]] = []
col: List[int] = []
col_size = 1
for idx in range(self.size):
if col_size == 3:
col_size = 1
cols.append(col)
col = [idx]
else:
col_size += 1
col.append(idx)
if len(col) > 0:
cols.append(col)
final_cols: List[List[int]] = [Hopscotch.space_at_column(idx) for idx in range(min(len(cols), self.width))]
for (col_idx, col) in enumerate(cols):
if col_idx >= self.width:
final_cols[col_idx % self.width].extend([-1 for _ in range(self.spacer)])
final_cols[col_idx % self.width].extend(col)
fill_to_longest(final_cols)
return final_cols
def idx_bottom(self) -> Set[int]:
corners = math.ceil(self.size / 3)
indices = [num * 3 + 1 for num in range(corners)]
return {
idx for idx in indices if idx < self.size
}
def idx_top(self) -> Set[int]:
corners = math.ceil(self.size / 3)
indices = [num * 3 + 2 for num in range(corners)]
return {
idx for idx in indices if idx < self.size
}
def idx_middle(self) -> Set[int]:
corners = math.ceil(self.size / 3)
indices = [num * 3 for num in range(corners)]
return {
idx for idx in indices if idx < self.size
}
def idx_corner(self, number: str) -> Union[Set[int], None]:
try:
number = int(number)
except:
return None
corners = math.ceil(self.size / 3)
if number >= corners:
return None
indices = [number * 3 + n for n in range(3)]
return {
idx for idx in indices if idx < self.size
}
class Gauntlet(LayoutType):
"""Long, linear layout. Goes horizontally and wraps around.
Default entrance is index 0 in the top left, default exit is index `size - 1` in the bottom right."""
width: int
# 0 1 2 3
#
# 4 5 6 7
def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]:
width: int = options.pop("width", 7)
self.width = min(max(width, 4), self.size)
return options
def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]:
missions = [mission_factory() for _ in range(self.size)]
missions[0].option_entrance = True
missions[-1].option_exit = True
for i in range(self.size - 1):
missions[i].next.append(missions[i + 1])
return missions
def get_visual_layout(self) -> List[List[int]]:
columns = [[] for _ in range(self.width)]
for idx in range(self.size):
if idx >= self.width:
columns[idx % self.width].append(-1)
columns[idx % self.width].append(idx)
fill_to_longest(columns)
return columns
class Blitz(LayoutType):
"""Rows of missions, one mission per row required.
Default entrances are every mission in the top row, default exit is a central mission in the bottom row."""
width: int
index_functions = [
"row"
]
# 0 1 2 3
# 4 5 6 7
def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]:
width = options.pop("width", 0)
if width < 1:
min_width, max_width = 2, 5
mission_divisor = 5
self.width = min(max(self.size // mission_divisor, min_width), max_width)
else:
self.width = min(self.size, width)
return options
def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]:
slots = [mission_factory() for _ in range(self.size)]
for idx in range(self.width):
slots[idx].option_entrance = True
# TODO: this is copied from the original mission order and works, but I'm not sure on the intent
# middle_column = self.width // 2
# if self.size % self.width > middle_column:
# final_row = self.width * (self.size // self.width)
# final_mission = final_row + middle_column
# else:
# final_mission = self.size - 1
# slots[final_mission].option_exit = True
rows = self.size // self.width
for row in range(rows):
for top in range(self.width):
idx = row * self.width + top
for bot in range(self.width):
other = (row + 1) * self.width + bot
if other < self.size:
slots[idx].next.append(slots[other])
if row == rows-1:
slots[idx].option_exit = True
return slots
def get_visual_layout(self) -> List[List[int]]:
columns = [[] for _ in range(self.width)]
for idx in range(self.size):
columns[idx % self.width].append(idx)
fill_to_longest(columns)
return columns
def idx_row(self, row: str) -> Union[Set[int], None]:
try:
row = int(row)
except:
return None
rows = math.ceil(self.size / self.width)
if row >= rows:
return None
indices = [row * self.width + col for col in range(self.width)]
return {
idx for idx in indices if idx < self.size
}
def fill_to_longest(columns: List[List[int]]):
longest = max(len(col) for col in columns)
for idx in range(len(columns)):
length = len(columns[idx])
if length < longest:
columns[idx].extend([-1 for _ in range(longest - length)])

View File

@@ -0,0 +1,251 @@
from enum import IntEnum
from typing import TYPE_CHECKING, Dict, Set, List, Iterable
from Options import OptionError
from ..mission_tables import SC2Mission, lookup_id_to_mission, MissionFlag, SC2Campaign
from worlds.AutoWorld import World
if TYPE_CHECKING:
from .nodes import SC2MOGenMission
class Difficulty(IntEnum):
RELATIVE = 0
STARTER = 1
EASY = 2
MEDIUM = 3
HARD = 4
VERY_HARD = 5
# TODO figure out an organic way to get these
DEFAULT_DIFFICULTY_THRESHOLDS = {
Difficulty.STARTER: 0,
Difficulty.EASY: 10,
Difficulty.MEDIUM: 35,
Difficulty.HARD: 65,
Difficulty.VERY_HARD: 90,
Difficulty.VERY_HARD + 1: 100
}
STANDARD_DIFFICULTY_FILL_ORDER = (
Difficulty.VERY_HARD,
Difficulty.STARTER,
Difficulty.HARD,
Difficulty.EASY,
Difficulty.MEDIUM,
)
"""Fill mission slots outer->inner difficulties,
so if multiple pools get exhausted, they will tend to overflow towards the middle."""
def modified_difficulty_thresholds(min_difficulty: Difficulty, max_difficulty: Difficulty) -> Dict[int, Difficulty]:
if min_difficulty == Difficulty.RELATIVE:
min_difficulty = Difficulty.STARTER
if max_difficulty == Difficulty.RELATIVE:
max_difficulty = Difficulty.VERY_HARD
thresholds: Dict[int, Difficulty] = {}
min_thresh = DEFAULT_DIFFICULTY_THRESHOLDS[min_difficulty]
total_thresh = DEFAULT_DIFFICULTY_THRESHOLDS[max_difficulty + 1] - min_thresh
for difficulty in range(min_difficulty, max_difficulty + 1):
threshold = DEFAULT_DIFFICULTY_THRESHOLDS[difficulty] - min_thresh
threshold *= 100 // total_thresh
thresholds[threshold] = Difficulty(difficulty)
return thresholds
class SC2MOGenMissionPools:
"""
Manages available and used missions for a mission order.
"""
master_list: Set[int]
difficulty_pools: Dict[Difficulty, Set[int]]
_used_flags: Dict[MissionFlag, int]
_used_missions: List[SC2Mission]
_updated_difficulties: Dict[int, Difficulty]
_flag_ratios: Dict[MissionFlag, float]
_flag_weights: Dict[MissionFlag, int]
def __init__(self) -> None:
self.master_list = {mission.id for mission in SC2Mission}
self.difficulty_pools = {
diff: {mission.id for mission in SC2Mission if mission.pool + 1 == diff}
for diff in Difficulty if diff != Difficulty.RELATIVE
}
self._used_flags = {}
self._used_missions = []
self._updated_difficulties = {}
self._flag_ratios = {}
self._flag_weights = {}
def set_exclusions(self, excluded: Iterable[SC2Mission], unexcluded: Iterable[SC2Mission]) -> None:
"""Prevents all the missions that appear in the `excluded` list, but not in the `unexcluded` list,
from appearing in the mission order."""
total_exclusions = [mission.id for mission in excluded if mission not in unexcluded]
self.master_list.difference_update(total_exclusions)
def get_allowed_mission_count(self) -> int:
return len(self.master_list)
def count_allowed_missions(self, campaign: SC2Campaign) -> int:
allowed_missions = [
mission_id
for mission_id in self.master_list
if lookup_id_to_mission[mission_id].campaign == campaign
]
return len(allowed_missions)
def move_mission(self, mission: SC2Mission, old_diff: Difficulty, new_diff: Difficulty) -> None:
"""Changes the difficulty of the given `mission`. Does nothing if the mission is not allowed to appear
or if it isn't set to the `old_diff` difficulty."""
if mission.id in self.master_list and mission.id in self.difficulty_pools[old_diff]:
self.difficulty_pools[old_diff].remove(mission.id)
self.difficulty_pools[new_diff].add(mission.id)
self._updated_difficulties[mission.id] = new_diff
def get_modified_mission_difficulty(self, mission: SC2Mission) -> Difficulty:
if mission.id in self._updated_difficulties:
return self._updated_difficulties[mission.id]
return Difficulty(mission.pool + 1)
def get_pool_size(self, diff: Difficulty) -> int:
"""Returns the amount of missions of the given difficulty that are allowed to appear."""
return len(self.difficulty_pools[diff])
def get_used_flags(self) -> Dict[MissionFlag, int]:
"""Returns a dictionary of all used flags and their appearance count within the mission order.
Flags that don't appear in the mission order also don't appear in this dictionary."""
return self._used_flags
def get_used_missions(self) -> List[SC2Mission]:
"""Returns a set of all missions used in the mission order."""
return self._used_missions
def set_flag_balances(self, flag_ratios: Dict[MissionFlag, int], flag_weights: Dict[MissionFlag, int]):
# Ensure the ratios are percentages
ratio_sum = sum(ratio for ratio in flag_ratios.values())
self._flag_ratios = {flag: ratio / ratio_sum for flag, ratio in flag_ratios.items()}
self._flag_weights = flag_weights
def pick_balanced_mission(self, world: World, pool: List[int]) -> int:
"""Applies ratio-based and weight-based balancing to pick a preferred mission from a given mission pool."""
# Currently only used for race balancing
# Untested for flags that may overlap or not be present at all, but should at least generate
balanced_pool = pool
if len(self._flag_ratios) > 0:
relevant_used_flag_count = max(sum(self._used_flags.get(flag, 0) for flag in self._flag_ratios), 1)
current_ratios = {
flag: self._used_flags.get(flag, 0) / relevant_used_flag_count
for flag in self._flag_ratios
}
# Desirability of missions is the difference between target and current ratios for relevant flags
flag_scores = {
flag: self._flag_ratios[flag] - current_ratios[flag]
for flag in self._flag_ratios
}
mission_scores = [
sum(
flag_scores[flag] for flag in self._flag_ratios
if flag in lookup_id_to_mission[mission].flags
)
for mission in balanced_pool
]
# Only keep the missions that create the best balance
best_score = max(mission_scores)
balanced_pool = [mission for idx, mission in enumerate(balanced_pool) if mission_scores[idx] == best_score]
balanced_weights = [1.0 for _ in balanced_pool]
if len(self._flag_weights) > 0:
relevant_used_flag_count = max(sum(self._used_flags.get(flag, 0) for flag in self._flag_weights), 1)
# Higher usage rate of relevant flags means lower desirability
flag_scores = {
flag: (relevant_used_flag_count - self._used_flags.get(flag, 0)) * self._flag_weights[flag]
for flag in self._flag_weights
}
# Mission scores are averaged across the mission's flags,
# else flags that aren't always present will inflate weights
mission_scores = [
sum(
flag_scores[flag] for flag in self._flag_weights
if flag in lookup_id_to_mission[mission].flags
) / sum(flag in lookup_id_to_mission[mission].flags for flag in self._flag_weights)
for mission in balanced_pool
]
balanced_weights = mission_scores
if sum(balanced_weights) == 0.0:
balanced_weights = [1.0 for _ in balanced_weights]
return world.random.choices(balanced_pool, balanced_weights, k=1)[0]
def pull_specific_mission(self, mission: SC2Mission) -> None:
"""Marks the given mission as present in the mission order."""
# Remove the mission from the master list and whichever difficulty pool it is in
if mission.id in self.master_list:
self.master_list.remove(mission.id)
for diff in self.difficulty_pools:
if mission.id in self.difficulty_pools[diff]:
self.difficulty_pools[diff].remove(mission.id)
break
self._add_mission_stats(mission)
def _add_mission_stats(self, mission: SC2Mission) -> None:
# Update used flag counts & missions
# Done weirdly for Python <= 3.10 compatibility
flag: MissionFlag
for flag in iter(MissionFlag): # type: ignore
if flag & mission.flags == flag:
self._used_flags.setdefault(flag, 0)
self._used_flags[flag] += 1
self._used_missions.append(mission)
def pull_random_mission(self, world: World, slot: 'SC2MOGenMission', *, prefer_close_difficulty: bool = False) -> SC2Mission:
"""Picks a random mission from the mission pool of the given slot and marks it as present in the mission order.
With `prefer_close_difficulty = True` the mission is picked to be as close to the slot's desired difficulty as possible."""
pool = slot.option_mission_pool.intersection(self.master_list)
difficulty_pools: Dict[int, List[int]] = {
diff: sorted(pool.intersection(self.difficulty_pools[diff]))
for diff in Difficulty if diff != Difficulty.RELATIVE
}
if len(pool) == 0:
raise OptionError(f"No available mission to be picked for slot {slot.get_address_to_node()}.")
desired_difficulty = slot.option_difficulty
if prefer_close_difficulty:
# Iteratively look up and down around the slot's desired difficulty
# Either a difficulty with valid missions is found, or an error is raised
difficulty_offset = 0
final_pool = difficulty_pools[desired_difficulty]
while len(final_pool) == 0:
higher_diff = min(desired_difficulty + difficulty_offset + 1, Difficulty.VERY_HARD)
final_pool = difficulty_pools[higher_diff]
if len(final_pool) > 0:
break
lower_diff = max(desired_difficulty - difficulty_offset, Difficulty.STARTER)
final_pool = difficulty_pools[lower_diff]
if len(final_pool) > 0:
break
if lower_diff == Difficulty.STARTER and higher_diff == Difficulty.VERY_HARD:
raise IndexError()
difficulty_offset += 1
else:
# Consider missions from all lower difficulties as well the desired difficulty
# Only take from higher difficulties if no lower difficulty is possible
final_pool = [
mission
for difficulty in range(Difficulty.STARTER, desired_difficulty + 1)
for mission in difficulty_pools[difficulty]
]
difficulty_offset = 1
while len(final_pool) == 0:
higher_difficulty = desired_difficulty + difficulty_offset
if higher_difficulty > Difficulty.VERY_HARD:
raise IndexError()
final_pool = difficulty_pools[higher_difficulty]
difficulty_offset += 1
# Remove the mission from the master list
mission = lookup_id_to_mission[self.pick_balanced_mission(world, final_pool)]
self.master_list.remove(mission.id)
self.difficulty_pools[self.get_modified_mission_difficulty(mission)].remove(mission.id)
self._add_mission_stats(mission)
return mission

View File

@@ -0,0 +1,606 @@
"""
Contains the data structures that make up a mission order.
Data in these structures is validated in .options.py and manipulated by .generation.py.
"""
from __future__ import annotations
from typing import Dict, Set, Callable, List, Any, Type, Optional, Union, TYPE_CHECKING
from weakref import ref, ReferenceType
from dataclasses import asdict
from abc import ABC, abstractmethod
import logging
from BaseClasses import Region, CollectionState
from ..mission_tables import SC2Mission
from ..item import item_names
from .layout_types import LayoutType
from .entry_rules import SubRuleEntryRule, ItemEntryRule
from .mission_pools import Difficulty
from .slot_data import CampaignSlotData, LayoutSlotData, MissionSlotData
if TYPE_CHECKING:
from .. import SC2World
class MissionOrderNode(ABC):
parent: Optional[ReferenceType[MissionOrderNode]]
important_beat_event: bool
def get_parent(self, address_so_far: str, full_address: str) -> MissionOrderNode:
if self.parent is None:
raise ValueError(
f"Address \"{address_so_far}\" (from \"{full_address}\") could not find a parent object. "
"This should mean the address contains \"..\" too often."
)
return self.parent()
@abstractmethod
def search(self, term: str) -> Union[List[MissionOrderNode], None]:
raise NotImplementedError
@abstractmethod
def child_type_name(self) -> str:
raise NotImplementedError
@abstractmethod
def get_missions(self) -> List[SC2MOGenMission]:
raise NotImplementedError
@abstractmethod
def get_exits(self) -> List[SC2MOGenMission]:
raise NotImplementedError
@abstractmethod
def get_visual_requirement(self, start_node: MissionOrderNode) -> Union[str, SC2MOGenMission]:
raise NotImplementedError
@abstractmethod
def get_key_name(self) -> str:
raise NotImplementedError
@abstractmethod
def get_min_depth(self) -> int:
raise NotImplementedError
@abstractmethod
def get_address_to_node(self) -> str:
raise NotImplementedError
class SC2MOGenMissionOrder(MissionOrderNode):
"""
The top-level data structure for mission orders.
"""
campaigns: List[SC2MOGenCampaign]
sorted_missions: Dict[Difficulty, List[SC2MOGenMission]]
"""All mission slots in the mission order sorted by their difficulty, but not their depth."""
fixed_missions: List[SC2MOGenMission]
"""All mission slots that have a plando'd mission."""
items_to_lock: Dict[str, int]
keys_to_resolve: Dict[MissionOrderNode, List[ItemEntryRule]]
goal_missions: List[SC2MOGenMission]
max_depth: int
def __init__(self, world: 'SC2World', data: Dict[str, Any]):
self.campaigns = []
self.sorted_missions = {diff: [] for diff in Difficulty if diff != Difficulty.RELATIVE}
self.fixed_missions = []
self.items_to_lock = {}
self.keys_to_resolve = {}
self.goal_missions = []
self.parent = None
for (campaign_name, campaign_data) in data.items():
campaign = SC2MOGenCampaign(world, ref(self), campaign_name, campaign_data)
self.campaigns.append(campaign)
# Check that the mission order actually has a goal
for campaign in self.campaigns:
if campaign.option_goal:
self.goal_missions.extend(mission for mission in campaign.exits)
for layout in campaign.layouts:
if layout.option_goal:
self.goal_missions.extend(layout.exits)
for mission in layout.missions:
if mission.option_goal and not mission.option_empty:
self.goal_missions.append(mission)
# Remove duplicates
for goal in self.goal_missions:
while self.goal_missions.count(goal) > 1:
self.goal_missions.remove(goal)
# If not, set the last defined campaign as goal
if len(self.goal_missions) == 0:
self.campaigns[-1].option_goal = True
self.goal_missions.extend(mission for mission in self.campaigns[-1].exits)
# Apply victory cache option wherever the value has not yet been defined; must happen after goal missions are decided
for mission in self.get_missions():
if mission.option_victory_cache != -1:
# Already set
continue
if mission in self.goal_missions:
mission.option_victory_cache = 0
else:
mission.option_victory_cache = world.options.victory_cache.value
# Resolve names
used_names: Set[str] = set()
for campaign in self.campaigns:
names = [campaign.option_name] if len(campaign.option_display_name) == 0 else campaign.option_display_name
if campaign.option_unique_name:
names = [name for name in names if name not in used_names]
campaign.display_name = world.random.choice(names)
used_names.add(campaign.display_name)
for layout in campaign.layouts:
names = [layout.option_name] if len(layout.option_display_name) == 0 else layout.option_display_name
if layout.option_unique_name:
names = [name for name in names if name not in used_names]
layout.display_name = world.random.choice(names)
used_names.add(layout.display_name)
def get_slot_data(self) -> List[Dict[str, Any]]:
# [(campaign data, [(layout data, [[(mission data)]] )] )]
return [asdict(campaign.get_slot_data()) for campaign in self.campaigns]
def search(self, term: str) -> Union[List[MissionOrderNode], None]:
return [
campaign.layouts[0] if campaign.option_single_layout_campaign else campaign
for campaign in self.campaigns
if campaign.option_name.casefold() == term.casefold()
]
def child_type_name(self) -> str:
return "Campaign"
def get_missions(self) -> List[SC2MOGenMission]:
return [mission for campaign in self.campaigns for layout in campaign.layouts for mission in layout.missions]
def get_exits(self) -> List[SC2MOGenMission]:
return []
def get_visual_requirement(self, _start_node: MissionOrderNode) -> Union[str, SC2MOGenMission]:
return "All Missions"
def get_key_name(self) -> str:
return super().get_key_name() # type: ignore
def get_min_depth(self) -> int:
return super().get_min_depth() # type: ignore
def get_address_to_node(self):
return self.campaigns[0].get_address_to_node() + "/.."
class SC2MOGenCampaign(MissionOrderNode):
option_name: str # name of this campaign
option_display_name: List[str]
option_unique_name: bool
option_entry_rules: List[Dict[str, Any]]
option_unique_progression_track: int # progressive keys under this campaign and on this track will be changed to a unique track
option_goal: bool # whether this campaign is required to beat the game
# minimum difficulty of this campaign
# 'relative': based on the median distance of the first mission
option_min_difficulty: Difficulty
# maximum difficulty of this campaign
# 'relative': based on the median distance of the last mission
option_max_difficulty: Difficulty
option_single_layout_campaign: bool
# layouts of this campaign in correct order
layouts: List[SC2MOGenLayout]
exits: List[SC2MOGenMission] # missions required to beat this campaign (missions marked "exit" in layouts marked "exit")
entry_rule: SubRuleEntryRule
display_name: str
min_depth: int
max_depth: int
def __init__(self, world: 'SC2World', parent: ReferenceType[SC2MOGenMissionOrder], name: str, data: Dict[str, Any]):
self.parent = parent
self.important_beat_event = False
self.option_name = name
self.option_display_name = data["display_name"]
self.option_unique_name = data["unique_name"]
self.option_goal = data["goal"]
self.option_entry_rules = data["entry_rules"]
self.option_unique_progression_track = data["unique_progression_track"]
self.option_min_difficulty = Difficulty(data["min_difficulty"])
self.option_max_difficulty = Difficulty(data["max_difficulty"])
self.option_single_layout_campaign = data["single_layout_campaign"]
self.layouts = []
self.exits = []
for (layout_name, layout_data) in data.items():
if type(layout_data) == dict:
layout = SC2MOGenLayout(world, ref(self), layout_name, layout_data)
self.layouts.append(layout)
# Collect required missions (marked layouts' exits)
if layout.option_exit:
self.exits.extend(layout.exits)
# If no exits are set, use the last defined layout
if len(self.exits) == 0:
self.layouts[-1].option_exit = True
self.exits.extend(self.layouts[-1].exits)
def is_beaten(self, beaten_missions: Set[SC2MOGenMission]) -> bool:
return beaten_missions.issuperset(self.exits)
def is_always_unlocked(self, in_region_creation = False) -> bool:
return self.entry_rule.is_always_fulfilled(in_region_creation)
def is_unlocked(self, beaten_missions: Set[SC2MOGenMission], in_region_creation = False) -> bool:
return self.entry_rule.is_fulfilled(beaten_missions, in_region_creation)
def search(self, term: str) -> Union[List[MissionOrderNode], None]:
return [
layout
for layout in self.layouts
if layout.option_name.casefold() == term.casefold()
]
def child_type_name(self) -> str:
return "Layout"
def get_missions(self) -> List[SC2MOGenMission]:
return [mission for layout in self.layouts for mission in layout.missions]
def get_exits(self) -> List[SC2MOGenMission]:
return self.exits
def get_visual_requirement(self, start_node: MissionOrderNode) -> Union[str, SC2MOGenMission]:
visual_name = self.get_visual_name()
# Needs special handling for double-parent, which is valid for missions but errors for campaigns
first_parent = start_node.get_parent("", "")
if (
first_parent is self or (
first_parent.parent is not None and first_parent.get_parent("", "") is self
)
) and visual_name == "":
return "this campaign"
return visual_name
def get_visual_name(self) -> str:
return self.display_name
def get_key_name(self) -> str:
return item_names._TEMPLATE_NAMED_CAMPAIGN_KEY.format(self.get_visual_name())
def get_min_depth(self) -> int:
return self.min_depth
def get_address_to_node(self) -> str:
return f"{self.option_name}"
def get_slot_data(self) -> CampaignSlotData:
if self.important_beat_event:
exits = [slot.mission.id for slot in self.exits]
else:
exits = []
return CampaignSlotData(
self.get_visual_name(),
asdict(self.entry_rule.to_slot_data()),
exits,
[asdict(layout.get_slot_data()) for layout in self.layouts]
)
class SC2MOGenLayout(MissionOrderNode):
option_name: str # name of this layout
option_display_name: List[str] # visual name of this layout
option_unique_name: bool
option_type: Type[LayoutType] # type of this layout
option_size: int # amount of missions in this layout
option_goal: bool # whether this layout is required to beat the game
option_exit: bool # whether this layout is required to beat its parent campaign
option_mission_pool: List[int] # IDs of valid missions for this layout
option_missions: List[Dict[str, Any]]
option_entry_rules: List[Dict[str, Any]]
option_unique_progression_track: int # progressive keys under this layout and on this track will be changed to a unique track
# minimum difficulty of this layout
# 'relative': based on the median distance of the first mission
option_min_difficulty: Difficulty
# maximum difficulty of this layout
# 'relative': based on the median distance of the last mission
option_max_difficulty: Difficulty
missions: List[SC2MOGenMission]
layout_type: LayoutType
entrances: List[SC2MOGenMission]
exits: List[SC2MOGenMission]
entry_rule: SubRuleEntryRule
display_name: str
min_depth: int
max_depth: int
def __init__(self, world: 'SC2World', parent: ReferenceType[SC2MOGenCampaign], name: str, data: Dict):
self.parent: ReferenceType[SC2MOGenCampaign] = parent
self.important_beat_event = False
self.option_name = name
self.option_display_name = data.pop("display_name")
self.option_unique_name = data.pop("unique_name")
self.option_type = data.pop("type")
self.option_size = data.pop("size")
self.option_goal = data.pop("goal")
self.option_exit = data.pop("exit")
self.option_mission_pool = data.pop("mission_pool")
self.option_missions = data.pop("missions")
self.option_entry_rules = data.pop("entry_rules")
self.option_unique_progression_track = data.pop("unique_progression_track")
self.option_min_difficulty = Difficulty(data.pop("min_difficulty"))
self.option_max_difficulty = Difficulty(data.pop("max_difficulty"))
self.missions = []
self.entrances = []
self.exits = []
# Check for positive size now instead of during YAML validation to actively error with default size
if self.option_size == 0:
raise ValueError(f"Layout \"{self.option_name}\" has a size of 0.")
# Build base layout
from . import layout_types
self.layout_type: LayoutType = getattr(layout_types, self.option_type)(self.option_size)
unused = self.layout_type.set_options(data)
if len(unused) > 0:
logging.warning(f"SC2 ({world.player_name}): Layout \"{self.option_name}\" has unknown options: {list(unused.keys())}")
mission_factory = lambda: SC2MOGenMission(ref(self), set(self.option_mission_pool))
self.missions = self.layout_type.make_slots(mission_factory)
# Update missions with user data
for mission_data in self.option_missions:
indices: Set[int] = set()
index_terms: List[Union[int, str]] = mission_data["index"]
for term in index_terms:
result = self.resolve_index_term(term)
indices.update(result)
for idx in indices:
self.missions[idx].update_with_data(mission_data)
# Let layout respond to user changes
self.layout_type.final_setup(self.missions)
for mission in self.missions:
if mission.option_entrance:
self.entrances.append(mission)
if mission.option_exit:
self.exits.append(mission)
if mission.option_next is not None:
mission.next = [self.missions[idx] for term in mission.option_next for idx in sorted(self.resolve_index_term(term))]
# Set up missions' prev data
for mission in self.missions:
for next_mission in mission.next:
next_mission.prev.append(mission)
# Remove empty missions from access data
for mission in self.missions:
if mission.option_empty:
for next_mission in mission.next:
next_mission.prev.remove(mission)
mission.next.clear()
for prev_mission in mission.prev:
prev_mission.next.remove(mission)
mission.prev.clear()
# Clean up data and options
all_empty = True
for mission in self.missions:
if mission.option_empty:
# Empty missions cannot be entrances, exits, or required
# This is done now instead of earlier to make "set all default entrances to empty" not fail
if mission in self.entrances:
self.entrances.remove(mission)
mission.option_entrance = False
if mission in self.exits:
self.exits.remove(mission)
mission.option_exit = False
mission.option_goal = False
# Empty missions are also not allowed to cause secondary effects via entry rules (eg. create key items)
mission.option_entry_rules = []
else:
all_empty = False
# Establish the following invariant:
# A non-empty mission has no prev missions <=> A non-empty mission is an entrance
# This is mandatory to guarantee the entire layout is accessible via consecutive .nexts
# Note that the opposite is not enforced for exits to allow fully optional layouts
if len(mission.prev) == 0:
mission.option_entrance = True
self.entrances.append(mission)
elif mission.option_entrance:
for prev_mission in mission.prev:
prev_mission.next.remove(mission)
mission.prev.clear()
if all_empty:
raise Exception(f"Layout \"{self.option_name}\" only contains empty mission slots.")
def is_beaten(self, beaten_missions: Set[SC2MOGenMission]) -> bool:
return beaten_missions.issuperset(self.exits)
def is_always_unlocked(self, in_region_creation = False) -> bool:
return self.entry_rule.is_always_fulfilled(in_region_creation)
def is_unlocked(self, beaten_missions: Set[SC2MOGenMission], in_region_creation = False) -> bool:
return self.entry_rule.is_fulfilled(beaten_missions, in_region_creation)
def resolve_index_term(self, term: Union[str, int], *, ignore_out_of_bounds: bool = True, reject_none: bool = True) -> Union[Set[int], None]:
try:
result = {int(term)}
except ValueError:
if term == "entrances":
result = {idx for idx in range(len(self.missions)) if self.missions[idx].option_entrance}
elif term == "exits":
result = {idx for idx in range(len(self.missions)) if self.missions[idx].option_exit}
elif term == "all":
result = {idx for idx in range(len(self.missions))}
else:
result = self.layout_type.parse_index(term)
if result is None and reject_none:
raise ValueError(f"Layout \"{self.option_name}\" could not resolve mission index term \"{term}\".")
if ignore_out_of_bounds:
result = [index for index in result if index >= 0 and index < len(self.missions)]
return result
def get_parent(self, _address_so_far: str, _full_address: str) -> MissionOrderNode:
if self.parent().option_single_layout_campaign:
parent = self.parent().parent
else:
parent = self.parent
return parent()
def search(self, term: str) -> Union[List[MissionOrderNode], None]:
indices = self.resolve_index_term(term, reject_none=False)
if indices is None:
# Let the address parser handle the fail case
return []
missions = [self.missions[index] for index in sorted(indices)]
return missions
def child_type_name(self) -> str:
return "Mission"
def get_missions(self) -> List[SC2MOGenMission]:
return [mission for mission in self.missions]
def get_exits(self) -> List[SC2MOGenMission]:
return self.exits
def get_visual_requirement(self, start_node: MissionOrderNode) -> Union[str, SC2MOGenMission]:
visual_name = self.get_visual_name()
if start_node.get_parent("", "") is self and visual_name == "":
return "this questline"
return visual_name
def get_visual_name(self) -> str:
return self.display_name
def get_key_name(self) -> str:
return item_names._TEMPLATE_NAMED_LAYOUT_KEY.format(self.get_visual_name(), self.parent().get_visual_name())
def get_min_depth(self) -> int:
return self.min_depth
def get_address_to_node(self) -> str:
campaign = self.parent()
if campaign.option_single_layout_campaign:
return f"{self.option_name}"
return self.parent().get_address_to_node() + f"/{self.option_name}"
def get_slot_data(self) -> LayoutSlotData:
mission_slots = [
[
asdict(self.missions[idx].get_slot_data() if (idx >= 0 and not self.missions[idx].option_empty) else MissionSlotData.empty())
for idx in column
]
for column in self.layout_type.get_visual_layout()
]
if self.important_beat_event:
exits = [slot.mission.id for slot in self.exits]
else:
exits = []
return LayoutSlotData(
self.get_visual_name(),
asdict(self.entry_rule.to_slot_data()),
exits,
mission_slots
)
class SC2MOGenMission(MissionOrderNode):
option_goal: bool # whether this mission is required to beat the game
option_entrance: bool # whether this mission is unlocked when the layout is unlocked
option_exit: bool # whether this mission is required to beat its parent layout
option_empty: bool # whether this slot contains a mission at all
option_next: Union[None, List[Union[int, str]]] # indices of internally connected missions
option_entry_rules: List[Dict[str, Any]]
option_difficulty: Difficulty # difficulty pool this mission pulls from
option_mission_pool: Set[int] # Allowed mission IDs for this slot
option_victory_cache: int # Number of victory cache locations tied to the mission name
entry_rule: SubRuleEntryRule
min_depth: int # Smallest amount of missions to beat before this slot is accessible
mission: SC2Mission
region: Region
next: List[SC2MOGenMission]
prev: List[SC2MOGenMission]
def __init__(self, parent: ReferenceType[SC2MOGenLayout], parent_mission_pool: Set[int]):
self.parent: ReferenceType[SC2MOGenLayout] = parent
self.important_beat_event = False
self.option_mission_pool = parent_mission_pool
self.option_goal = False
self.option_entrance = False
self.option_exit = False
self.option_empty = False
self.option_next = None
self.option_entry_rules = []
self.option_difficulty = Difficulty.RELATIVE
self.next = []
self.prev = []
self.min_depth = -1
self.option_victory_cache = -1
def update_with_data(self, data: Dict):
self.option_goal = data.get("goal", self.option_goal)
self.option_entrance = data.get("entrance", self.option_entrance)
self.option_exit = data.get("exit", self.option_exit)
self.option_empty = data.get("empty", self.option_empty)
self.option_next = data.get("next", self.option_next)
self.option_entry_rules = data.get("entry_rules", self.option_entry_rules)
self.option_difficulty = data.get("difficulty", self.option_difficulty)
self.option_mission_pool = data.get("mission_pool", self.option_mission_pool)
self.option_victory_cache = data.get("victory_cache", -1)
def is_always_unlocked(self, in_region_creation = False) -> bool:
return self.entry_rule.is_always_fulfilled(in_region_creation)
def is_unlocked(self, beaten_missions: Set[SC2MOGenMission], in_region_creation = False) -> bool:
return self.entry_rule.is_fulfilled(beaten_missions, in_region_creation)
def beat_item(self) -> str:
return f"Beat {self.mission.mission_name}"
def beat_rule(self, player) -> Callable[[CollectionState], bool]:
return lambda state: state.has(self.beat_item(), player)
def search(self, term: str) -> Union[List[MissionOrderNode], None]:
return None
def child_type_name(self) -> str:
return ""
def get_missions(self) -> List[SC2MOGenMission]:
return [self]
def get_exits(self) -> List[SC2MOGenMission]:
return [self]
def get_visual_requirement(self, _start_node: MissionOrderNode) -> Union[str, SC2MOGenMission]:
return self
def get_key_name(self) -> str:
return item_names._TEMPLATE_MISSION_KEY.format(self.mission.mission_name)
def get_min_depth(self) -> int:
return self.min_depth
def get_address_to_node(self) -> str:
layout = self.parent()
assert layout is not None
index = layout.missions.index(self)
return layout.get_address_to_node() + f"/{index}"
def get_slot_data(self) -> MissionSlotData:
return MissionSlotData(
self.mission.id,
[mission.mission.id for mission in self.prev],
self.entry_rule.to_slot_data(),
self.option_victory_cache,
)

View File

@@ -0,0 +1,472 @@
"""
Contains the Custom Mission Order option. Also validates the option value, so generation can assume the data matches the specification.
"""
from __future__ import annotations
import random
from Options import OptionDict, Visibility
from schema import Schema, Optional, And, Or
import typing
from typing import Any, Union, Dict, Set, List
import copy
from ..mission_tables import lookup_name_to_mission
from ..mission_groups import mission_groups
from ..item.item_tables import item_table
from ..item.item_groups import item_name_groups
from . import layout_types
from .layout_types import LayoutType, Column, Grid, Hopscotch, Gauntlet, Blitz, Canvas
from .mission_pools import Difficulty
from .presets_static import (
static_preset, preset_mini_wol_with_prophecy, preset_mini_wol, preset_mini_hots, preset_mini_prophecy,
preset_mini_lotv_prologue, preset_mini_lotv, preset_mini_lotv_epilogue, preset_mini_nco,
preset_wol_with_prophecy, preset_wol, preset_prophecy, preset_hots, preset_lotv_prologue,
preset_lotv_epilogue, preset_lotv, preset_nco
)
from .presets_scripted import make_golden_path
GENERIC_KEY_NAME = "Key".casefold()
GENERIC_PROGRESSIVE_KEY_NAME = "Progressive Key".casefold()
STR_OPTION_VALUES: Dict[str, Dict[str, Any]] = {
"type": {
"column": Column.__name__, "grid": Grid.__name__, "hopscotch": Hopscotch.__name__, "gauntlet": Gauntlet.__name__, "blitz": Blitz.__name__,
"canvas": Canvas.__name__,
},
"difficulty": {
"relative": Difficulty.RELATIVE.value, "starter": Difficulty.STARTER.value, "easy": Difficulty.EASY.value,
"medium": Difficulty.MEDIUM.value, "hard": Difficulty.HARD.value, "very hard": Difficulty.VERY_HARD.value
},
"preset": {
"none": lambda _: {},
"wol + prophecy": static_preset(preset_wol_with_prophecy),
"wol": static_preset(preset_wol),
"prophecy": static_preset(preset_prophecy),
"hots": static_preset(preset_hots),
"prologue": static_preset(preset_lotv_prologue),
"lotv prologue": static_preset(preset_lotv_prologue),
"lotv": static_preset(preset_lotv),
"epilogue": static_preset(preset_lotv_epilogue),
"lotv epilogue": static_preset(preset_lotv_epilogue),
"nco": static_preset(preset_nco),
"mini wol + prophecy": static_preset(preset_mini_wol_with_prophecy),
"mini wol": static_preset(preset_mini_wol),
"mini prophecy": static_preset(preset_mini_prophecy),
"mini hots": static_preset(preset_mini_hots),
"mini prologue": static_preset(preset_mini_lotv_prologue),
"mini lotv prologue": static_preset(preset_mini_lotv_prologue),
"mini lotv": static_preset(preset_mini_lotv),
"mini epilogue": static_preset(preset_mini_lotv_epilogue),
"mini lotv epilogue": static_preset(preset_mini_lotv_epilogue),
"mini nco": static_preset(preset_mini_nco),
"golden path": make_golden_path
},
}
STR_OPTION_VALUES["min_difficulty"] = STR_OPTION_VALUES["difficulty"]
STR_OPTION_VALUES["max_difficulty"] = STR_OPTION_VALUES["difficulty"]
GLOBAL_ENTRY = "global"
StrOption = lambda cat: And(str, lambda val: val.lower() in STR_OPTION_VALUES[cat])
IntNegOne = And(int, lambda val: val >= -1)
IntZero = And(int, lambda val: val >= 0)
IntOne = And(int, lambda val: val >= 1)
IntPercent = And(int, lambda val: 0 <= val <= 100)
IntZeroToTen = And(int, lambda val: 0 <= val <= 10)
SubRuleEntryRule = {
"rules": [{str: object}], # recursive schema checking is too hard
"amount": IntNegOne,
}
MissionCountEntryRule = {
"scope": [str],
"amount": IntNegOne,
}
BeatMissionsEntryRule = {
"scope": [str],
}
ItemEntryRule = {
"items": {str: int}
}
EntryRule = Or(SubRuleEntryRule, MissionCountEntryRule, BeatMissionsEntryRule, ItemEntryRule)
SchemaDifficulty = Or(*[value.value for value in Difficulty])
class CustomMissionOrder(OptionDict):
"""
Used to generate a custom mission order. Please see documentation to understand usage.
Will do nothing unless `mission_order` is set to `custom`.
"""
display_name = "Custom Mission Order"
visibility = Visibility.template
value: Dict[str, Dict[str, Any]]
default = {
"Default Campaign": {
"display_name": "null",
"unique_name": False,
"entry_rules": [],
"unique_progression_track": 0,
"goal": True,
"min_difficulty": "relative",
"max_difficulty": "relative",
GLOBAL_ENTRY: {
"display_name": "null",
"unique_name": False,
"entry_rules": [],
"unique_progression_track": 0,
"goal": False,
"exit": False,
"mission_pool": ["all missions"],
"min_difficulty": "relative",
"max_difficulty": "relative",
"missions": [],
},
"Default Layout": {
"type": "grid",
"size": 9,
},
},
}
schema = Schema({
# Campaigns
str: {
"display_name": [str],
"unique_name": bool,
"entry_rules": [EntryRule],
"unique_progression_track": int,
"goal": bool,
"min_difficulty": SchemaDifficulty,
"max_difficulty": SchemaDifficulty,
"single_layout_campaign": bool,
# Layouts
str: {
"display_name": [str],
"unique_name": bool,
# Type options
"type": lambda val: issubclass(getattr(layout_types, val), LayoutType),
"size": IntOne,
# Link options
"exit": bool,
"goal": bool,
"entry_rules": [EntryRule],
"unique_progression_track": int,
# Mission pool options
"mission_pool": {int},
"min_difficulty": SchemaDifficulty,
"max_difficulty": SchemaDifficulty,
# Allow arbitrary options for layout types
Optional(str): Or(int, str, bool, [Or(int, str, bool)]),
# Mission slots
"missions": [{
"index": [Or(int, str)],
Optional("entrance"): bool,
Optional("exit"): bool,
Optional("goal"): bool,
Optional("empty"): bool,
Optional("next"): [Or(int, str)],
Optional("entry_rules"): [EntryRule],
Optional("mission_pool"): {int},
Optional("difficulty"): SchemaDifficulty,
Optional("victory_cache"): IntZeroToTen,
}],
},
}
})
def __init__(self, yaml_value: Dict[str, Dict[str, Any]]) -> None:
# This function constructs self.value by parts,
# so the parent constructor isn't called
self.value: Dict[str, Dict[str, Any]] = {}
if yaml_value == self.default: # If this option is default, it shouldn't mess with its own values
yaml_value = copy.deepcopy(self.default)
for campaign in yaml_value:
self.value[campaign] = {}
# Check if this campaign has a layout type, making it a campaign-level layout
single_layout_campaign = "type" in yaml_value[campaign]
if single_layout_campaign:
# Single-layout campaigns are not allowed to declare more layouts
single_layout = {key: val for (key, val) in yaml_value[campaign].items() if type(val) != dict}
yaml_value[campaign] = {campaign: single_layout}
# Campaign should inherit certain values from the layout
if "goal" not in single_layout or not single_layout["goal"]:
yaml_value[campaign]["goal"] = False
if "unique_progression_track" in single_layout:
yaml_value[campaign]["unique_progression_track"] = single_layout["unique_progression_track"]
# Hide campaign name for single-layout campaigns
yaml_value[campaign]["display_name"] = ""
yaml_value[campaign]["single_layout_campaign"] = single_layout_campaign
# Check if this campaign has a global layout
global_dict = {}
for name in yaml_value[campaign]:
if name.lower() == GLOBAL_ENTRY:
global_dict = yaml_value[campaign].pop(name)
break
# Strip layouts and unknown options from the campaign
# The latter are assumed to be preset options
preset_key: str = yaml_value[campaign].pop("preset", "none")
layout_keys = [key for (key, val) in yaml_value[campaign].items() if type(val) == dict]
layouts = {key: yaml_value[campaign].pop(key) for key in layout_keys}
preset_option_keys = [key for key in yaml_value[campaign] if key not in self.default["Default Campaign"]]
preset_option_keys.remove("single_layout_campaign")
preset_options = {key: yaml_value[campaign].pop(key) for key in preset_option_keys}
# Resolve preset
preset: Dict[str, Any] = _resolve_string_option_single("preset", preset_key)(preset_options)
# Preset global is resolved internally to avoid conflict with user global
preset_global_dict = {}
for name in preset:
if name.lower() == GLOBAL_ENTRY:
preset_global_dict = preset.pop(name)
break
preset_layout_keys = [key for (key, val) in preset.items() if type(val) == dict]
preset_layouts = {key: preset.pop(key) for key in preset_layout_keys}
ordered_layouts = {key: copy.deepcopy(preset_global_dict) for key in preset_layout_keys}
for key in preset_layout_keys:
ordered_layouts[key].update(preset_layouts[key])
# Final layouts are preset layouts (updated by same-name user layouts) followed by custom user layouts
for key in layouts:
if key in ordered_layouts:
# Mission slots for presets should go before user mission slots
if "missions" in layouts[key] and "missions" in ordered_layouts[key]:
layouts[key]["missions"] = ordered_layouts[key]["missions"] + layouts[key]["missions"]
ordered_layouts[key].update(layouts[key])
else:
ordered_layouts[key] = layouts[key]
# Campaign values = default options (except for default layouts) + preset options (except for layouts) + campaign options
self.value[campaign] = {key: value for (key, value) in self.default["Default Campaign"].items() if type(value) != dict}
self.value[campaign].update(preset)
self.value[campaign].update(yaml_value[campaign])
_resolve_special_options(self.value[campaign])
for layout in ordered_layouts:
# Layout values = default options + campaign's global options + layout options
self.value[campaign][layout] = copy.deepcopy(self.default["Default Campaign"][GLOBAL_ENTRY])
self.value[campaign][layout].update(global_dict)
self.value[campaign][layout].update(ordered_layouts[layout])
_resolve_special_options(self.value[campaign][layout])
for mission_slot_index in range(len(self.value[campaign][layout]["missions"])):
# Defaults for mission slots are handled by the mission slot struct
_resolve_special_options(self.value[campaign][layout]["missions"][mission_slot_index])
# Overloaded to remove pre-init schema validation
# Schema is still validated after __init__
@classmethod
def from_any(cls, data: Dict[str, Any]) -> CustomMissionOrder:
if type(data) == dict:
return cls(data)
else:
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
def _resolve_special_options(data: Dict[str, Any]):
# Handle range values & string-to-value conversions
for option in data:
option_value = data[option]
new_value = _resolve_special_option(option, option_value)
data[option] = new_value
# Special case for canvas layouts determining their own size
if "type" in data and data["type"] == Canvas.__name__:
canvas: List[str] = data["canvas"]
longest_line = max(len(line) for line in canvas)
data["size"] = len(canvas) * longest_line
data["width"] = longest_line
def _resolve_special_option(option: str, option_value: Any) -> Any:
# Option values can be string representations of values
if option in STR_OPTION_VALUES:
return _resolve_string_option(option, option_value)
if option == "mission_pool":
return _resolve_mission_pool(option_value)
if option == "entry_rules":
rules = [_resolve_entry_rule(subrule) for subrule in option_value]
return rules
if option == "display_name":
# Make sure all the values are strings
if type(option_value) == list:
names = [str(value) for value in option_value]
return names
elif option_value == "null":
# "null" means no custom display name
return []
else:
return [str(option_value)]
if option in ["index", "next"]:
# All index values could be ranges
if type(option_value) == list:
# Flatten any nested lists
indices = [idx for val in [idx if type(idx) == list else [idx] for idx in option_value] for idx in val]
indices = [_resolve_potential_range(index) for index in indices]
indices = [idx if type(idx) == int else str(idx) for idx in indices]
return indices
else:
idx = _resolve_potential_range(option_value)
return [idx if type(idx) == int else str(idx)]
# Option values can be ranges
return _resolve_potential_range(option_value)
def _resolve_string_option_single(option: str, option_value: str) -> Any:
formatted_value = option_value.lower().replace("_", " ")
if formatted_value not in STR_OPTION_VALUES[option]:
raise ValueError(
f"Option \"{option}\" received unknown value \"{option_value}\".\n"
f"Allowed values are: {list(STR_OPTION_VALUES[option].keys())}"
)
return STR_OPTION_VALUES[option][formatted_value]
def _resolve_string_option(option: str, option_value: Union[List[str], str]) -> Any:
if type(option_value) == list:
return [_resolve_string_option_single(option, val) for val in option_value]
else:
return _resolve_string_option_single(option, option_value)
def _resolve_entry_rule(option_value: Dict[str, Any]) -> Dict[str, Any]:
resolved: Dict[str, Any] = {}
mutually_exclusive: List[str] = []
if "amount" in option_value:
resolved["amount"] = _resolve_potential_range(option_value["amount"])
if "scope" in option_value:
mutually_exclusive.append("scope")
# A scope may be a list or a single address
if type(option_value["scope"]) == list:
resolved["scope"] = [str(subscope) for subscope in option_value["scope"]]
else:
resolved["scope"] = [str(option_value["scope"])]
if "rules" in option_value:
mutually_exclusive.append("rules")
resolved["rules"] = [_resolve_entry_rule(subrule) for subrule in option_value["rules"]]
# Make sure sub-rule rules have a specified amount
if "amount" not in option_value:
resolved["amount"] = -1
if "items" in option_value:
mutually_exclusive.append("items")
option_items: Dict[str, Any] = option_value["items"]
resolved_items = {item: int(_resolve_potential_range(str(amount))) for (item, amount) in option_items.items()}
resolved_items = _resolve_item_names(resolved_items)
resolved["items"] = {}
for item in resolved_items:
if item not in item_table:
if item.casefold() == GENERIC_KEY_NAME or item.casefold().startswith(GENERIC_PROGRESSIVE_KEY_NAME):
resolved["items"][item] = max(0, resolved_items[item])
continue
raise ValueError(f"Item rule contains \"{item}\", which is not a valid item name.")
amount = max(0, resolved_items[item])
quantity = item_table[item].quantity
if amount == 0:
final_amount = quantity
elif quantity == 0:
final_amount = amount
else:
final_amount = amount
resolved["items"][item] = final_amount
if len(mutually_exclusive) > 1:
raise ValueError(
"Entry rule contains too many identifiers.\n"
f"Rule: {option_value}\n"
f"Remove all but one of these entries: {mutually_exclusive}"
)
return resolved
def _resolve_potential_range(option_value: Union[Any, str]) -> Union[Any, int]:
# An option value may be a range
if type(option_value) == str and option_value.startswith("random-range-"):
resolved = _custom_range(option_value)
return resolved
else:
# As this is a catch-all function,
# assume non-range option values are handled elsewhere
# or intended to fall through
return option_value
def _resolve_mission_pool(option_value: Union[str, List[str]]) -> Set[int]:
if type(option_value) == str:
pool = _get_target_missions(option_value)
else:
pool: Set[int] = set()
for line in option_value:
if line.startswith("~"):
if len(pool) == 0:
raise ValueError(f"Mission Pool term {line} tried to remove missions from an empty pool.")
term = line[1:].strip()
missions = _get_target_missions(term)
pool.difference_update(missions)
elif line.startswith("^"):
if len(pool) == 0:
raise ValueError(f"Mission Pool term {line} tried to remove missions from an empty pool.")
term = line[1:].strip()
missions = _get_target_missions(term)
pool.intersection_update(missions)
else:
if line.startswith("+"):
term = line[1:].strip()
else:
term = line.strip()
missions = _get_target_missions(term)
pool.update(missions)
if len(pool) == 0:
raise ValueError(f"Mission pool evaluated to zero missions: {option_value}")
return pool
def _get_target_missions(term: str) -> Set[int]:
if term in lookup_name_to_mission:
return {lookup_name_to_mission[term].id}
else:
groups = [mission_groups[group] for group in mission_groups if group.casefold() == term.casefold()]
if len(groups) > 0:
return {lookup_name_to_mission[mission].id for mission in groups[0]}
else:
raise ValueError(f"Mission pool term \"{term}\" did not resolve to any specific mission or mission group.")
# Class-agnostic version of AP Options.Range.custom_range
def _custom_range(text: str) -> int:
textsplit = text.split("-")
try:
random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])]
except ValueError:
raise ValueError(f"Invalid random range {text} for option {CustomMissionOrder.__name__}")
random_range.sort()
if text.startswith("random-range-low"):
return _triangular(random_range[0], random_range[1], random_range[0])
elif text.startswith("random-range-middle"):
return _triangular(random_range[0], random_range[1])
elif text.startswith("random-range-high"):
return _triangular(random_range[0], random_range[1], random_range[1])
else:
return random.randint(random_range[0], random_range[1])
def _triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int:
return int(round(random.triangular(lower, end, tri), 0))
# Version of options.Sc2ItemDict.verify without World
def _resolve_item_names(value: Dict[str, int]) -> Dict[str, int]:
new_value: dict[str, int] = {}
case_insensitive_group_mapping = {
group_name.casefold(): group_value for group_name, group_value in item_name_groups.items()
}
case_insensitive_group_mapping.update({item.casefold(): {item} for item in item_table})
for group_name in value:
item_names = case_insensitive_group_mapping.get(group_name.casefold(), {group_name})
for item_name in item_names:
new_value[item_name] = new_value.get(item_name, 0) + value[group_name]
return new_value

View File

@@ -0,0 +1,164 @@
from typing import Dict, Any, List
import copy
def _required_option(option: str, options: Dict[str, Any]) -> Any:
"""Returns the option value, or raises an error if the option is not present."""
if option not in options:
raise KeyError(f"Campaign preset is missing required option \"{option}\".")
return options.pop(option)
def _validate_option(option: str, options: Dict[str, str], default: str, valid_values: List[str]) -> str:
"""Returns the option value if it is present and valid, the default if it is not present, or raises an error if it is present but not valid."""
result = options.pop(option, default)
if result not in valid_values:
raise ValueError(f"Preset option \"{option}\" received unknown value \"{result}\".")
return result
def make_golden_path(options: Dict[str, Any]) -> Dict[str, Any]:
chain_name_options = ['Mar Sara', 'Agria', 'Redstone', 'Meinhoff', 'Haven', 'Tarsonis', 'Valhalla', 'Char',
'Umoja', 'Kaldir', 'Zerus', 'Skygeirr Station', 'Dominion Space', 'Korhal',
'Aiur', 'Glacius', 'Shakuras', 'Ulnar', 'Slayn',
'Antiga', 'Braxis', 'Chau Sara', 'Moria', 'Tyrador', 'Xil', 'Zhakul',
'Azeroth', 'Crouton', 'Draenor', 'Sanctuary']
size = max(_required_option("size", options), 4)
keys_option_values = ["none", "layouts", "missions", "progressive_layouts", "progressive_missions", "progressive_per_layout"]
keys_option = _validate_option("keys", options, "none", keys_option_values)
min_chains = 2
max_chains = 6
two_start_positions = options.pop("two_start_positions", False)
# Compensating for empty mission at start
if two_start_positions:
size += 1
class Campaign:
def __init__(self, missions_remaining: int):
self.chain_lengths = [1]
self.chain_padding = [0]
self.required_missions = [0]
self.padding = 0
self.missions_remaining = missions_remaining
self.mission_counter = 1
def add_mission(self, chain: int, required_missions: int = 0, *, is_final: bool = False):
if self.missions_remaining == 0 and not is_final:
return
self.mission_counter += 1
self.chain_lengths[chain] += 1
self.missions_remaining -= 1
if chain == 0:
self.padding += 1
self.required_missions.append(required_missions)
def add_chain(self):
self.chain_lengths.append(0)
self.chain_padding.append(self.padding)
campaign = Campaign(size - 2)
current_required_missions = 0
main_chain_length = 0
while campaign.missions_remaining > 0:
main_chain_length += 1
if main_chain_length % 2 == 1: # Adding branches
chains_to_make = 0 if len(campaign.chain_lengths) >= max_chains else min_chains if main_chain_length == 1 else 1
for _ in range(chains_to_make):
campaign.add_chain()
# Updating branches
for side_chain in range(len(campaign.chain_lengths) - 1, 0, -1):
campaign.add_mission(side_chain)
# Adding main path mission
current_required_missions = (campaign.mission_counter * 3) // 4
if two_start_positions:
# Compensating for skipped mission at start
current_required_missions -= 1
campaign.add_mission(0, current_required_missions)
campaign.add_mission(0, current_required_missions, is_final = True)
# Create mission order preset out of campaign
layout_base = {
"type": "column",
"display_name": chain_name_options,
"unique_name": True,
"missions": [],
}
# Optionally add key requirement to layouts
if keys_option == "layouts":
layout_base["entry_rules"] = [{ "items": { "Key": 1 }}]
elif keys_option == "progressive_layouts":
layout_base["entry_rules"] = [{ "items": { "Progressive Key": 0 }}]
preset = {
str(chain): copy.deepcopy(layout_base) for chain in range(len(campaign.chain_lengths))
}
preset["0"]["exit"] = True
if not two_start_positions:
preset["0"].pop("entry_rules", [])
for chain in range(len(campaign.chain_lengths)):
length = campaign.chain_lengths[chain]
padding = campaign.chain_padding[chain]
preset[str(chain)]["size"] = padding + length
# Add padding to chain
if padding > 0:
preset[str(chain)]["missions"].append({
"index": [pad for pad in range(padding)],
"empty": True
})
if chain == 0:
if two_start_positions:
preset["0"]["missions"].append({
"index": 0,
"empty": True
})
# Main path gets number requirements
for mission in range(1, len(campaign.required_missions)):
preset["0"]["missions"].append({
"index": mission,
"entry_rules": [{
"scope": "../..",
"amount": campaign.required_missions[mission]
}]
})
# Optionally add key requirements except to the starter mission
if keys_option == "missions":
for slot in preset["0"]["missions"]:
if "entry_rules" in slot:
slot["entry_rules"].append({ "items": { "Key": 1 }})
elif keys_option == "progressive_missions":
for slot in preset["0"]["missions"]:
if "entry_rules" in slot:
slot["entry_rules"].append({ "items": { "Progressive Key": 1 }})
# No main chain keys for progressive_per_layout keys
else:
# Other paths get main path requirements
if two_start_positions and chain < 3:
preset[str(chain)].pop("entry_rules", [])
for mission in range(length):
target = padding + mission
if two_start_positions and mission == 0 and chain < 3:
preset[str(chain)]["missions"].append({
"index": target,
"entrance": True
})
else:
preset[str(chain)]["missions"].append({
"index": target,
"entry_rules": [{
"scope": f"../../0/{target}"
}]
})
# Optionally add key requirements
if keys_option == "missions":
for slot in preset[str(chain)]["missions"]:
if "entry_rules" in slot:
slot["entry_rules"].append({ "items": { "Key": 1 }})
elif keys_option == "progressive_missions":
for slot in preset[str(chain)]["missions"]:
if "entry_rules" in slot:
slot["entry_rules"].append({ "items": { "Progressive Key": 1 }})
elif keys_option == "progressive_per_layout":
for slot in preset[str(chain)]["missions"]:
if "entry_rules" in slot:
slot["entry_rules"].append({ "items": { "Progressive Key": 0 }})
return preset

View File

@@ -0,0 +1,916 @@
from typing import Dict, Any, Callable, List, Tuple
import copy
from ..mission_groups import MissionGroupNames
from ..mission_tables import SC2Mission, SC2Campaign
preset_mini_wol_with_prophecy = {
"global": {
"type": "column",
"mission_pool": [
MissionGroupNames.WOL_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
]
},
"Mar Sara": {
"size": 1
},
"Colonist": {
"size": 2,
"entry_rules": [
{ "scope": "../Mar Sara" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Artifact": {
"size": 3,
"entry_rules": [
{ "scope": "../Mar Sara" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 1, "entry_rules": [{ "scope": "../..", "amount": 4 }, { "items": { "Key": 1 }}] },
{ "index": 2, "entry_rules": [{ "scope": "../..", "amount": 8 }, { "items": { "Key": 1 }}] }
]
},
"Prophecy": {
"size": 2,
"entry_rules": [
{ "scope": "../Artifact/1" },
{ "items": { "Key": 1 }}
],
"mission_pool": [
MissionGroupNames.PROPHECY_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Covert": {
"size": 2,
"entry_rules": [
{ "scope": "../Mar Sara" },
{ "scope": "..", "amount": 2 },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Rebellion": {
"size": 2,
"entry_rules": [
{ "scope": "../Mar Sara" },
{ "scope": "..", "amount": 3 },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Char": {
"size": 3,
"entry_rules": [
{ "scope": "../Artifact" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "next": [2] },
{ "index": 1, "entrance": True }
]
}
}
preset_mini_wol = copy.deepcopy(preset_mini_wol_with_prophecy)
preset_mini_prophecy = { "Prophecy": preset_mini_wol.pop("Prophecy") }
preset_mini_prophecy["Prophecy"].pop("entry_rules")
preset_mini_prophecy["Prophecy"]["type"] = "gauntlet"
preset_mini_prophecy["Prophecy"]["display_name"] = ""
preset_mini_prophecy["Prophecy"]["missions"].append({ "index": "entrances", "entry_rules": [] })
preset_mini_hots = {
"global": {
"type": "column",
"mission_pool": [
MissionGroupNames.HOTS_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
]
},
"Umoja": {
"size": 1,
},
"Kaldir": {
"size": 2,
"entry_rules": [
{ "scope": "../Umoja" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Char": {
"size": 2,
"entry_rules": [
{ "scope": "../Umoja" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Zerus": {
"size": 2,
"entry_rules": [
{ "scope": "../Umoja" },
{ "scope": "..", "amount": 3 },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Skygeirr Station": {
"size": 2,
"entry_rules": [
{ "scope": "../Zerus" },
{ "scope": "..", "amount": 5 },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Dominion Space": {
"size": 2,
"entry_rules": [
{ "scope": "../Zerus" },
{ "scope": "..", "amount": 5 },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Korhal": {
"size": 2,
"entry_rules": [
{ "scope": "../Zerus" },
{ "scope": "..", "amount": 8 },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
}
}
preset_mini_lotv_prologue = {
"min_difficulty": "easy",
"Prologue": {
"display_name": "",
"type": "gauntlet",
"size": 2,
"mission_pool": [
MissionGroupNames.PROLOGUE_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
],
"missions": [
{ "index": 1, "entry_rules": [{ "items": { "Key": 1 }}] }
]
}
}
preset_mini_lotv = {
"global": {
"type": "column",
"mission_pool": [
MissionGroupNames.LOTV_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
]
},
"Aiur": {
"size": 2,
"missions": [
{ "index": 1, "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Korhal": {
"size": 1,
"entry_rules": [
{ "scope": "../Aiur" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Shakuras": {
"size": 1,
"entry_rules": [
{ "scope": "../Aiur" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Purifier": {
"size": 2,
"entry_rules": [
{ "scope": "../Korhal" },
{ "scope": "../Shakuras" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 1, "entry_rules": [{ "scope": "../../Ulnar" }, { "items": { "Key": 1 }}] }
]
},
"Ulnar": {
"size": 1,
"entry_rules": [
{ "scope": "../Purifier/0" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Tal'darim": {
"size": 1,
"entry_rules": [
{ "scope": "../Ulnar" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Return to Aiur": {
"size": 2,
"entry_rules": [
{ "scope": "../Purifier" },
{ "scope": "../Tal'darim" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
}
}
preset_mini_lotv_epilogue = {
"min_difficulty": "very hard",
"Epilogue": {
"display_name": "",
"type": "gauntlet",
"size": 2,
"mission_pool": [
MissionGroupNames.EPILOGUE_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
],
"missions": [
{ "index": 1, "entry_rules": [{ "items": { "Key": 1 }}] }
]
}
}
preset_mini_nco = {
"min_difficulty": "easy",
"global": {
"type": "column",
"mission_pool": [
MissionGroupNames.NCO_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
]
},
"Mission Pack 1": {
"size": 2,
"missions": [
{ "index": 1, "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Mission Pack 2": {
"size": 1,
"entry_rules": [
{ "scope": "../Mission Pack 1" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Mission Pack 3": {
"size": 2,
"entry_rules": [
{ "scope": "../Mission Pack 2" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
}
preset_wol_with_prophecy = {
"global": {
"type": "column",
"mission_pool": [
MissionGroupNames.WOL_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
]
},
"Mar Sara": {
"size": 3,
"missions": [
{ "index": 0, "mission_pool": SC2Mission.LIBERATION_DAY.mission_name },
{ "index": 1, "mission_pool": SC2Mission.THE_OUTLAWS.mission_name },
{ "index": 2, "mission_pool": SC2Mission.ZERO_HOUR.mission_name },
{ "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] }
]
},
"Colonist": {
"size": 4,
"entry_rules": [
{ "scope": "../Mar Sara" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": 1, "next": [2, 3] },
{ "index": 2, "next": [] },
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": [2, 3], "entry_rules": [{ "scope": "../..", "amount": 7 }, { "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.EVACUATION.mission_name },
{ "index": 1, "mission_pool": SC2Mission.OUTBREAK.mission_name },
{ "index": 2, "mission_pool": SC2Mission.SAFE_HAVEN.mission_name },
{ "index": 3, "mission_pool": SC2Mission.HAVENS_FALL.mission_name },
]
},
"Artifact": {
"size": 5,
"entry_rules": [
{ "scope": "../Mar Sara" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 1, "entry_rules": [{ "scope": "../..", "amount": 8 }, { "items": { "Key": 1 }}] },
{ "index": 2, "entry_rules": [{ "scope": "../..", "amount": 11 }, { "items": { "Key": 1 }}] },
{ "index": 3, "entry_rules": [{ "scope": "../..", "amount": 14 }, { "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.SMASH_AND_GRAB.mission_name },
{ "index": 1, "mission_pool": SC2Mission.THE_DIG.mission_name },
{ "index": 2, "mission_pool": SC2Mission.THE_MOEBIUS_FACTOR.mission_name },
{ "index": 3, "mission_pool": SC2Mission.SUPERNOVA.mission_name },
{ "index": 4, "mission_pool": SC2Mission.MAW_OF_THE_VOID.mission_name },
]
},
"Prophecy": {
"size": 4,
"entry_rules": [
{ "scope": "../Artifact/1" },
{ "items": { "Key": 1 }}
],
"mission_pool": [
MissionGroupNames.PROPHECY_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.WHISPERS_OF_DOOM.mission_name },
{ "index": 1, "mission_pool": SC2Mission.A_SINISTER_TURN.mission_name },
{ "index": 2, "mission_pool": SC2Mission.ECHOES_OF_THE_FUTURE.mission_name },
{ "index": 3, "mission_pool": SC2Mission.IN_UTTER_DARKNESS.mission_name },
]
},
"Covert": {
"size": 4,
"entry_rules": [
{ "scope": "../Mar Sara" },
{ "scope": "..", "amount": 4 },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": 1, "next": [2, 3] },
{ "index": 2, "next": [] },
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": [2, 3], "entry_rules": [{ "scope": "../..", "amount": 8 }, { "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.DEVILS_PLAYGROUND.mission_name },
{ "index": 1, "mission_pool": SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name },
{ "index": 2, "mission_pool": SC2Mission.BREAKOUT.mission_name },
{ "index": 3, "mission_pool": SC2Mission.GHOST_OF_A_CHANCE.mission_name },
]
},
"Rebellion": {
"size": 5,
"entry_rules": [
{ "scope": "../Mar Sara" },
{ "scope": "..", "amount": 6 },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name },
{ "index": 1, "mission_pool": SC2Mission.CUTTHROAT.mission_name },
{ "index": 2, "mission_pool": SC2Mission.ENGINE_OF_DESTRUCTION.mission_name },
{ "index": 3, "mission_pool": SC2Mission.MEDIA_BLITZ.mission_name },
{ "index": 4, "mission_pool": SC2Mission.PIERCING_OF_THE_SHROUD.mission_name },
]
},
"Char": {
"size": 4,
"entry_rules": [
{ "scope": "../Artifact" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": 0, "next": [1, 2] },
{ "index": [1, 2], "next": [3] },
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.GATES_OF_HELL.mission_name },
{ "index": 1, "mission_pool": SC2Mission.BELLY_OF_THE_BEAST.mission_name },
{ "index": 2, "mission_pool": SC2Mission.SHATTER_THE_SKY.mission_name },
{ "index": 3, "mission_pool": SC2Mission.ALL_IN.mission_name },
]
}
}
preset_wol = copy.deepcopy(preset_wol_with_prophecy)
preset_prophecy = { "Prophecy": preset_wol.pop("Prophecy") }
preset_prophecy["Prophecy"].pop("entry_rules")
preset_prophecy["Prophecy"]["type"] = "gauntlet"
preset_prophecy["Prophecy"]["display_name"] = ""
preset_prophecy["Prophecy"]["missions"].append({ "index": "entrances", "entry_rules": [] })
preset_hots = {
"global": {
"type": "column",
"mission_pool": [
MissionGroupNames.HOTS_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
]
},
"Umoja": {
"size": 3,
"missions": [
{ "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.LAB_RAT.mission_name },
{ "index": 1, "mission_pool": SC2Mission.BACK_IN_THE_SADDLE.mission_name },
{ "index": 2, "mission_pool": SC2Mission.RENDEZVOUS.mission_name },
]
},
"Kaldir": {
"size": 3,
"entry_rules": [
{ "scope": "../Umoja" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.HARVEST_OF_SCREAMS.mission_name },
{ "index": 1, "mission_pool": SC2Mission.SHOOT_THE_MESSENGER.mission_name },
{ "index": 2, "mission_pool": SC2Mission.ENEMY_WITHIN.mission_name },
]
},
"Char": {
"size": 3,
"entry_rules": [
{ "scope": "../Umoja" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.DOMINATION.mission_name },
{ "index": 1, "mission_pool": SC2Mission.FIRE_IN_THE_SKY.mission_name },
{ "index": 2, "mission_pool": SC2Mission.OLD_SOLDIERS.mission_name },
]
},
"Zerus": {
"size": 3,
"entry_rules": [
{
"rules": [
{ "scope": "../Kaldir" },
{ "scope": "../Char" }
],
"amount": 1
},
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.WAKING_THE_ANCIENT.mission_name },
{ "index": 1, "mission_pool": SC2Mission.THE_CRUCIBLE.mission_name },
{ "index": 2, "mission_pool": SC2Mission.SUPREME.mission_name },
]
},
"Skygeirr Station": {
"size": 3,
"entry_rules": [
{ "scope": ["../Kaldir", "../Char", "../Zerus"] },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.INFESTED.mission_name },
{ "index": 1, "mission_pool": SC2Mission.HAND_OF_DARKNESS.mission_name },
{ "index": 2, "mission_pool": SC2Mission.PHANTOMS_OF_THE_VOID.mission_name },
]
},
"Dominion Space": {
"size": 2,
"entry_rules": [
{ "scope": ["../Kaldir", "../Char", "../Zerus"] },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.WITH_FRIENDS_LIKE_THESE.mission_name },
{ "index": 1, "mission_pool": SC2Mission.CONVICTION.mission_name },
]
},
"Korhal": {
"size": 3,
"entry_rules": [
{ "scope": ["../Skygeirr Station", "../Dominion Space"] },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.PLANETFALL.mission_name },
{ "index": 1, "mission_pool": SC2Mission.DEATH_FROM_ABOVE.mission_name },
{ "index": 2, "mission_pool": SC2Mission.THE_RECKONING.mission_name },
]
}
}
preset_lotv_prologue = {
"min_difficulty": "easy",
"Prologue": {
"display_name": "",
"type": "gauntlet",
"size": 3,
"mission_pool": [
MissionGroupNames.PROLOGUE_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
],
"missions": [
{ "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.DARK_WHISPERS.mission_name },
{ "index": 1, "mission_pool": SC2Mission.GHOSTS_IN_THE_FOG.mission_name },
{ "index": 2, "mission_pool": SC2Mission.EVIL_AWOKEN.mission_name },
]
}
}
preset_lotv = {
"global": {
"type": "column",
"mission_pool": [
MissionGroupNames.LOTV_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
]
},
"Aiur": {
"size": 3,
"missions": [
{ "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.FOR_AIUR.mission_name },
{ "index": 1, "mission_pool": SC2Mission.THE_GROWING_SHADOW.mission_name },
{ "index": 2, "mission_pool": SC2Mission.THE_SPEAR_OF_ADUN.mission_name },
]
},
"Korhal": {
"size": 2,
"entry_rules": [
{ "scope": "../Aiur" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.SKY_SHIELD.mission_name },
{ "index": 1, "mission_pool": SC2Mission.BROTHERS_IN_ARMS.mission_name },
]
},
"Shakuras": {
"size": 2,
"entry_rules": [
{ "scope": "../Aiur" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.AMON_S_REACH.mission_name },
{ "index": 1, "mission_pool": SC2Mission.LAST_STAND.mission_name },
]
},
"Purifier": {
"size": 3,
"entry_rules": [
{
"rules": [
{ "scope": "../Korhal" },
{ "scope": "../Shakuras" }
],
"amount": 1
},
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 1, "entry_rules": [{ "scope": "../../Ulnar" }, { "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.FORBIDDEN_WEAPON.mission_name },
{ "index": 1, "mission_pool": SC2Mission.UNSEALING_THE_PAST.mission_name },
{ "index": 2, "mission_pool": SC2Mission.PURIFICATION.mission_name },
]
},
"Ulnar": {
"size": 3,
"entry_rules": [
{
"scope": [
"../Korhal",
"../Shakuras",
"../Purifier/0"
]
},
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.TEMPLE_OF_UNIFICATION.mission_name },
{ "index": 1, "mission_pool": SC2Mission.THE_INFINITE_CYCLE.mission_name },
{ "index": 2, "mission_pool": SC2Mission.HARBINGER_OF_OBLIVION.mission_name },
]
},
"Tal'darim": {
"size": 2,
"entry_rules": [
{ "scope": "../Ulnar" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.STEPS_OF_THE_RITE.mission_name },
{ "index": 1, "mission_pool": SC2Mission.RAK_SHIR.mission_name },
]
},
"Moebius": {
"size": 1,
"entry_rules": [
{
"rules": [
{ "scope": "../Purifier" },
{ "scope": "../Tal'darim" }
],
"amount": 1
},
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.TEMPLAR_S_CHARGE.mission_name },
]
},
"Return to Aiur": {
"size": 3,
"entry_rules": [
{ "scope": "../Purifier" },
{ "scope": "../Tal'darim" },
{ "scope": "../Moebius" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.TEMPLAR_S_RETURN.mission_name },
{ "index": 1, "mission_pool": SC2Mission.THE_HOST.mission_name },
{ "index": 2, "mission_pool": SC2Mission.SALVATION.mission_name },
]
}
}
preset_lotv_epilogue = {
"min_difficulty": "very hard",
"Epilogue": {
"display_name": "",
"type": "gauntlet",
"size": 3,
"mission_pool": [
MissionGroupNames.EPILOGUE_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
],
"missions": [
{ "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.INTO_THE_VOID.mission_name },
{ "index": 1, "mission_pool": SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name },
{ "index": 2, "mission_pool": SC2Mission.AMON_S_FALL.mission_name },
]
}
}
preset_nco = {
"min_difficulty": "easy",
"global": {
"type": "column",
"mission_pool": [
MissionGroupNames.NCO_MISSIONS,
"~ " + MissionGroupNames.RACESWAP_MISSIONS
]
},
"Mission Pack 1": {
"size": 3,
"missions": [
{ "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.THE_ESCAPE.mission_name },
{ "index": 1, "mission_pool": SC2Mission.SUDDEN_STRIKE.mission_name },
{ "index": 2, "mission_pool": SC2Mission.ENEMY_INTELLIGENCE.mission_name },
]
},
"Mission Pack 2": {
"size": 3,
"entry_rules": [
{ "scope": "../Mission Pack 1" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.TROUBLE_IN_PARADISE.mission_name },
{ "index": 1, "mission_pool": SC2Mission.NIGHT_TERRORS.mission_name },
{ "index": 2, "mission_pool": SC2Mission.FLASHPOINT.mission_name },
]
},
"Mission Pack 3": {
"size": 3,
"entry_rules": [
{ "scope": "../Mission Pack 2" },
{ "items": { "Key": 1 }}
],
"missions": [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": 0, "mission_pool": SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name },
{ "index": 1, "mission_pool": SC2Mission.DARK_SKIES.mission_name },
{ "index": 2, "mission_pool": SC2Mission.END_GAME.mission_name },
]
},
}
def _build_static_preset(preset: Dict[str, Any], options: Dict[str, Any]) -> Dict[str, Any]:
# Raceswap shuffling
raceswaps = options.pop("shuffle_raceswaps", False)
if not isinstance(raceswaps, bool):
raise ValueError(
f"Preset option \"shuffle_raceswaps\" received unknown value \"{raceswaps}\".\n"
"Valid values are: true, false"
)
elif raceswaps == True:
# Remove "~ Raceswap Missions" operation from mission pool options
# Also add raceswap variants to plando'd vanilla missions
for layout in preset.values():
if type(layout) == dict:
# Currently mission pools in layouts are always ["X campaign missions", "~ raceswap missions"]
layout_mission_pool: List[str] = layout.get("mission_pool", None)
if layout_mission_pool is not None:
layout_mission_pool.pop()
layout["mission_pool"] = layout_mission_pool
if "missions" in layout:
for slot in layout["missions"]:
# Currently mission pools in slots are always strings
slot_mission_pool: str = slot.get("mission_pool", None)
# Identify raceswappable missions by their race in brackets
if slot_mission_pool is not None and slot_mission_pool[-1] == ")":
mission_name = slot_mission_pool[:slot_mission_pool.rfind("(")]
new_mission_pool = [f"{mission_name}({race})" for race in ["Terran", "Zerg", "Protoss"]]
slot["mission_pool"] = new_mission_pool
# The presets are set up for no raceswaps, so raceswaps == False doesn't need to be covered
# Mission pool selection
missions = options.pop("missions", "random")
if missions == "vanilla":
pass # use preset as it is
elif missions == "vanilla_shuffled":
# remove pre-set missions
for layout in preset.values():
if type(layout) == dict and "missions" in layout:
for slot in layout["missions"]:
slot.pop("mission_pool", ())
elif missions == "random":
# remove pre-set missions and mission pools
for layout in preset.values():
if type(layout) == dict:
layout.pop("mission_pool", ())
if "missions" in layout:
for slot in layout["missions"]:
slot.pop("mission_pool", ())
else:
raise ValueError(
f"Preset option \"missions\" received unknown value \"{missions}\".\n"
"Valid values are: random, vanilla, vanilla_shuffled"
)
# Key rule selection
keys = options.pop("keys", "none")
if keys == "layouts":
# remove keys from mission entry rules
for layout in preset.values():
if type(layout) == dict and "missions" in layout:
for slot in layout["missions"]:
if "entry_rules" in slot:
slot["entry_rules"] = _remove_key_rules(slot["entry_rules"])
elif keys == "missions":
# remove keys from layout entry rules
for layout in preset.values():
if type(layout) == dict and "entry_rules" in layout:
layout["entry_rules"] = _remove_key_rules(layout["entry_rules"])
elif keys == "progressive_layouts":
# remove keys from mission entry rules, replace keys in layout entry rules with unique-track keys
for layout in preset.values():
if type(layout) == dict:
if "entry_rules" in layout:
layout["entry_rules"] = _make_key_rules_progressive(layout["entry_rules"], 0)
if "missions" in layout:
for slot in layout["missions"]:
if "entry_rules" in slot:
slot["entry_rules"] = _remove_key_rules(slot["entry_rules"])
elif keys == "progressive_missions":
# remove keys from layout entry rules, replace keys in mission entry rules
for layout in preset.values():
if type(layout) == dict:
if "entry_rules" in layout:
layout["entry_rules"] = _remove_key_rules(layout["entry_rules"])
if "missions" in layout:
for slot in layout["missions"]:
if "entry_rules" in slot:
slot["entry_rules"] = _make_key_rules_progressive(slot["entry_rules"], 1)
elif keys == "progressive_per_layout":
# remove keys from layout entry rules, replace keys in mission entry rules with unique-track keys
# specifically ignore layouts that have no entry rules (and are thus the first of their campaign)
for layout in preset.values():
if type(layout) == dict and "entry_rules" in layout:
layout["entry_rules"] = _remove_key_rules(layout["entry_rules"])
if "missions" in layout:
for slot in layout["missions"]:
if "entry_rules" in slot:
slot["entry_rules"] = _make_key_rules_progressive(slot["entry_rules"], 0)
elif keys == "none":
# remove keys from both layout and mission entry rules
for layout in preset.values():
if type(layout) == dict:
if "entry_rules" in layout:
layout["entry_rules"] = _remove_key_rules(layout["entry_rules"])
if "missions" in layout:
for slot in layout["missions"]:
if "entry_rules" in slot:
slot["entry_rules"] = _remove_key_rules(slot["entry_rules"])
else:
raise ValueError(
f"Preset option \"keys\" received unknown value \"{keys}\".\n"
"Valid values are: none, missions, layouts, progressive_missions, progressive_layouts, progressive_per_layout"
)
return preset
def _remove_key_rules(entry_rules: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
return [rule for rule in entry_rules if not ("items" in rule and "Key" in rule["items"])]
def _make_key_rules_progressive(entry_rules: List[Dict[str, Any]], track: int) -> List[Dict[str, Any]]:
for rule in entry_rules:
if "items" in rule and "Key" in rule["items"]:
new_items: Dict[str, Any] = {}
for (item, amount) in rule["items"].items():
if item == "Key":
new_items["Progressive Key"] = track
else:
new_items[item] = amount
rule["items"] = new_items
return entry_rules
def static_preset(preset: Dict[str, Any]) -> Callable[[Dict[str, Any]], Dict[str, Any]]:
return lambda options: _build_static_preset(copy.deepcopy(preset), options)
def get_used_layout_names() -> Dict[SC2Campaign, Tuple[int, List[str]]]:
campaign_to_preset: Dict[SC2Campaign, Dict[str, Any]] = {
SC2Campaign.WOL: preset_wol_with_prophecy,
SC2Campaign.PROPHECY: preset_prophecy,
SC2Campaign.HOTS: preset_hots,
SC2Campaign.PROLOGUE: preset_lotv_prologue,
SC2Campaign.LOTV: preset_lotv,
SC2Campaign.EPILOGUE: preset_lotv_epilogue,
SC2Campaign.NCO: preset_nco
}
campaign_to_layout_names: Dict[SC2Campaign, Tuple[int, List[str]]] = { SC2Campaign.GLOBAL: (0, []) }
for campaign in SC2Campaign:
if campaign == SC2Campaign.GLOBAL:
continue
previous_campaign = [prev for prev in SC2Campaign if prev.id == campaign.id - 1][0]
previous_size = campaign_to_layout_names[previous_campaign][0]
preset = campaign_to_preset[campaign]
new_layouts = [value for value in preset.keys() if isinstance(preset[value], dict) and value != "global"]
campaign_to_layout_names[campaign] = (previous_size + len(campaign_to_layout_names[previous_campaign][1]), new_layouts)
campaign_to_layout_names.pop(SC2Campaign.GLOBAL)
return campaign_to_layout_names

View File

@@ -0,0 +1,53 @@
"""
Houses the data structures representing a mission order in slot data.
Creating these is handled by the nodes they represent in .nodes.py.
"""
from __future__ import annotations
from typing import List, Protocol
from dataclasses import dataclass
from .entry_rules import SubRuleRuleData
class MissionOrderObjectSlotData(Protocol):
entry_rule: SubRuleRuleData
@dataclass
class CampaignSlotData:
name: str
entry_rule: SubRuleRuleData
exits: List[int]
layouts: List[LayoutSlotData]
@staticmethod
def legacy(name: str, layouts: List[LayoutSlotData]) -> CampaignSlotData:
return CampaignSlotData(name, SubRuleRuleData.empty(), [], layouts)
@dataclass
class LayoutSlotData:
name: str
entry_rule: SubRuleRuleData
exits: List[int]
missions: List[List[MissionSlotData]]
@staticmethod
def legacy(name: str, missions: List[List[MissionSlotData]]) -> LayoutSlotData:
return LayoutSlotData(name, SubRuleRuleData.empty(), [], missions)
@dataclass
class MissionSlotData:
mission_id: int
prev_mission_ids: List[int]
entry_rule: SubRuleRuleData
victory_cache_size: int = 0
@staticmethod
def empty() -> MissionSlotData:
return MissionSlotData(-1, [], SubRuleRuleData.empty())
@staticmethod
def legacy(mission_id: int, prev_mission_ids: List[int], entry_rule: SubRuleRuleData) -> MissionSlotData:
return MissionSlotData(mission_id, prev_mission_ids, entry_rule)

View File

@@ -0,0 +1,577 @@
from typing import NamedTuple, Dict, List, Set, Union, Literal, Iterable, Optional
from enum import IntEnum, Enum, IntFlag, auto
class SC2Race(IntEnum):
ANY = 0
TERRAN = 1
ZERG = 2
PROTOSS = 3
def get_title(self):
return self.name.lower().capitalize()
def get_mission_flag(self):
return MissionFlag.__getitem__(self.get_title())
class MissionPools(IntEnum):
STARTER = 0
EASY = 1
MEDIUM = 2
HARD = 3
VERY_HARD = 4
FINAL = 5
class MissionFlag(IntFlag):
none = 0
Terran = auto()
Zerg = auto()
Protoss = auto()
NoBuild = auto()
Defense = auto()
AutoScroller = auto() # The mission is won by waiting out a timer or victory is gated behind a timer
Countdown = auto() # Overall, the mission must be beaten before a loss timer counts down
Kerrigan = auto() # The player controls Kerrigan in the mission
VanillaSoa = auto() # The player controls the Spear of Adun in the vanilla version of the mission
Nova = auto() # The player controls NCO Nova in the mission
AiTerranAlly = auto() # The mission has a Terran AI ally that can be taken over
AiZergAlly = auto() # The mission has a Zerg AI ally that can be taken over
AiProtossAlly = auto() # The mission has a Protoss AI ally that can be taken over
VsTerran = auto()
VsZerg = auto()
VsProtoss = auto()
HasRaceSwap = auto() # The mission has variants that use different factions from the vanilla experience.
RaceSwap = auto() # The mission uses different factions from the vanilla experience.
WoLNova = auto() # The player controls WoL Nova in the mission
AiAlly = AiTerranAlly|AiZergAlly|AiProtossAlly
TimedDefense = AutoScroller|Defense
VsTZ = VsTerran|VsZerg
VsTP = VsTerran|VsProtoss
VsPZ = VsProtoss|VsZerg
VsAll = VsTerran|VsProtoss|VsZerg
class SC2CampaignGoalPriority(IntEnum):
"""
Campaign's priority to goal election
"""
NONE = 0
MINI_CAMPAIGN = 1 # A goal shouldn't be in a mini-campaign if there's at least one 'big' campaign
HARD = 2 # A campaign ending with a hard mission
VERY_HARD = 3 # A campaign ending with a very hard mission
EPILOGUE = 4 # Epilogue shall be always preferred as the goal if present
class SC2Campaign(Enum):
def __new__(cls, *args, **kwargs):
value = len(cls.__members__) + 1
obj = object.__new__(cls)
obj._value_ = value
return obj
def __init__(self, campaign_id: int, name: str, goal_priority: SC2CampaignGoalPriority, race: SC2Race):
self.id = campaign_id
self.campaign_name = name
self.goal_priority = goal_priority
self.race = race
def __lt__(self, other: "SC2Campaign"):
return self.id < other.id
GLOBAL = 0, "Global", SC2CampaignGoalPriority.NONE, SC2Race.ANY
WOL = 1, "Wings of Liberty", SC2CampaignGoalPriority.VERY_HARD, SC2Race.TERRAN
PROPHECY = 2, "Prophecy", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS
HOTS = 3, "Heart of the Swarm", SC2CampaignGoalPriority.VERY_HARD, SC2Race.ZERG
PROLOGUE = 4, "Whispers of Oblivion (Legacy of the Void: Prologue)", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS
LOTV = 5, "Legacy of the Void", SC2CampaignGoalPriority.VERY_HARD, SC2Race.PROTOSS
EPILOGUE = 6, "Into the Void (Legacy of the Void: Epilogue)", SC2CampaignGoalPriority.EPILOGUE, SC2Race.ANY
NCO = 7, "Nova Covert Ops", SC2CampaignGoalPriority.HARD, SC2Race.TERRAN
class SC2Mission(Enum):
def __new__(cls, *args, **kwargs):
value = len(cls.__members__) + 1
obj = object.__new__(cls)
obj._value_ = value
return obj
def __init__(self, mission_id: int, name: str, campaign: SC2Campaign, area: str, race: SC2Race, pool: MissionPools, map_file: str, flags: MissionFlag):
self.id = mission_id
self.mission_name = name
self.campaign = campaign
self.area = area
self.race = race
self.pool = pool
self.map_file = map_file
self.flags = flags
def get_short_name(self):
if self.mission_name.find(' (') == -1:
return self.mission_name
else:
return self.mission_name[:self.mission_name.find(' (')]
# Wings of Liberty
LIBERATION_DAY = 1, "Liberation Day", SC2Campaign.WOL, "Mar Sara", SC2Race.ANY, MissionPools.STARTER, "ap_liberation_day", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsTerran
THE_OUTLAWS = 2, "The Outlaws (Terran)", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_the_outlaws", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
ZERO_HOUR = 3, "Zero Hour (Terran)", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_zero_hour", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
EVACUATION = 4, "Evacuation (Terran)", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_evacuation", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
OUTBREAK = 5, "Outbreak (Terran)", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_outbreak", MissionFlag.Terran|MissionFlag.Defense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
SAFE_HAVEN = 6, "Safe Haven (Terran)", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_safe_haven", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
HAVENS_FALL = 7, "Haven's Fall (Terran)", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_havens_fall", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
SMASH_AND_GRAB = 8, "Smash and Grab (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.EASY, "ap_smash_and_grab", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsPZ|MissionFlag.HasRaceSwap
THE_DIG = 9, "The Dig (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_dig", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
THE_MOEBIUS_FACTOR = 10, "The Moebius Factor (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_moebius_factor", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
SUPERNOVA = 11, "Supernova (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.HARD, "ap_supernova", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
MAW_OF_THE_VOID = 12, "Maw of the Void (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.HARD, "ap_maw_of_the_void", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
DEVILS_PLAYGROUND = 13, "Devil's Playground (Terran)", SC2Campaign.WOL, "Covert", SC2Race.TERRAN, MissionPools.EASY, "ap_devils_playground", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
WELCOME_TO_THE_JUNGLE = 14, "Welcome to the Jungle (Terran)", SC2Campaign.WOL, "Covert", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_welcome_to_the_jungle", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
BREAKOUT = 15, "Breakout", SC2Campaign.WOL, "Covert", SC2Race.ANY, MissionPools.STARTER, "ap_breakout", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsTerran
GHOST_OF_A_CHANCE = 16, "Ghost of a Chance", SC2Campaign.WOL, "Covert", SC2Race.ANY, MissionPools.STARTER, "ap_ghost_of_a_chance", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsTerran|MissionFlag.WoLNova
THE_GREAT_TRAIN_ROBBERY = 17, "The Great Train Robbery (Terran)", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.EASY, "ap_the_great_train_robbery", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
CUTTHROAT = 18, "Cutthroat (Terran)", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_cutthroat", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
ENGINE_OF_DESTRUCTION = 19, "Engine of Destruction (Terran)", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.HARD, "ap_engine_of_destruction", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
MEDIA_BLITZ = 20, "Media Blitz (Terran)", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_media_blitz", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
PIERCING_OF_THE_SHROUD = 21, "Piercing the Shroud", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.STARTER, "ap_piercing_the_shroud", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsAll
GATES_OF_HELL = 26, "Gates of Hell (Terran)", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.HARD, "ap_gates_of_hell", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
BELLY_OF_THE_BEAST = 27, "Belly of the Beast", SC2Campaign.WOL, "Char", SC2Race.ANY, MissionPools.STARTER, "ap_belly_of_the_beast", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsZerg
SHATTER_THE_SKY = 28, "Shatter the Sky (Terran)", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_shatter_the_sky", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
ALL_IN = 29, "All-In (Terran)", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_all_in", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
# Prophecy
WHISPERS_OF_DOOM = 22, "Whispers of Doom", SC2Campaign.PROPHECY, "_1", SC2Race.ANY, MissionPools.STARTER, "ap_whispers_of_doom", MissionFlag.Protoss|MissionFlag.NoBuild|MissionFlag.VsZerg
A_SINISTER_TURN = 23, "A Sinister Turn (Protoss)", SC2Campaign.PROPHECY, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_a_sinister_turn", MissionFlag.Protoss|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
ECHOES_OF_THE_FUTURE = 24, "Echoes of the Future (Protoss)", SC2Campaign.PROPHECY, "_3", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_echoes_of_the_future", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
IN_UTTER_DARKNESS = 25, "In Utter Darkness (Protoss)", SC2Campaign.PROPHECY, "_4", SC2Race.PROTOSS, MissionPools.HARD, "ap_in_utter_darkness", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
# Heart of the Swarm
LAB_RAT = 30, "Lab Rat (Zerg)", SC2Campaign.HOTS, "Umoja", SC2Race.ZERG, MissionPools.STARTER, "ap_lab_rat", MissionFlag.Zerg|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
BACK_IN_THE_SADDLE = 31, "Back in the Saddle", SC2Campaign.HOTS, "Umoja", SC2Race.ANY, MissionPools.STARTER, "ap_back_in_the_saddle", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.NoBuild|MissionFlag.VsTZ
RENDEZVOUS = 32, "Rendezvous (Zerg)", SC2Campaign.HOTS, "Umoja", SC2Race.ZERG, MissionPools.EASY, "ap_rendezvous", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
HARVEST_OF_SCREAMS = 33, "Harvest of Screams (Zerg)", SC2Campaign.HOTS, "Kaldir", SC2Race.ZERG, MissionPools.EASY, "ap_harvest_of_screams", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
SHOOT_THE_MESSENGER = 34, "Shoot the Messenger (Zerg)", SC2Campaign.HOTS, "Kaldir", SC2Race.ZERG, MissionPools.EASY, "ap_shoot_the_messenger", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.TimedDefense|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
ENEMY_WITHIN = 35, "Enemy Within", SC2Campaign.HOTS, "Kaldir", SC2Race.ANY, MissionPools.EASY, "ap_enemy_within", MissionFlag.Zerg|MissionFlag.NoBuild|MissionFlag.VsProtoss
DOMINATION = 36, "Domination (Zerg)", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.EASY, "ap_domination", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
FIRE_IN_THE_SKY = 37, "Fire in the Sky (Zerg)", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.MEDIUM, "ap_fire_in_the_sky", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
OLD_SOLDIERS = 38, "Old Soldiers (Zerg)", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.MEDIUM, "ap_old_soldiers", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
WAKING_THE_ANCIENT = 39, "Waking the Ancient (Zerg)", SC2Campaign.HOTS, "Zerus", SC2Race.ZERG, MissionPools.MEDIUM, "ap_waking_the_ancient", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
THE_CRUCIBLE = 40, "The Crucible (Zerg)", SC2Campaign.HOTS, "Zerus", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_crucible", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
SUPREME = 41, "Supreme", SC2Campaign.HOTS, "Zerus", SC2Race.ANY, MissionPools.MEDIUM, "ap_supreme", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.NoBuild|MissionFlag.VsZerg
INFESTED = 42, "Infested (Zerg)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.MEDIUM, "ap_infested", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
HAND_OF_DARKNESS = 43, "Hand of Darkness (Zerg)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.HARD, "ap_hand_of_darkness", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
PHANTOMS_OF_THE_VOID = 44, "Phantoms of the Void (Zerg)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.MEDIUM, "ap_phantoms_of_the_void", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
WITH_FRIENDS_LIKE_THESE = 45, "With Friends Like These", SC2Campaign.HOTS, "Dominion Space", SC2Race.ANY, MissionPools.STARTER, "ap_with_friends_like_these", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsTerran
CONVICTION = 46, "Conviction", SC2Campaign.HOTS, "Dominion Space", SC2Race.ANY, MissionPools.MEDIUM, "ap_conviction", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.NoBuild|MissionFlag.VsTerran
PLANETFALL = 47, "Planetfall (Zerg)", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_planetfall", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
DEATH_FROM_ABOVE = 48, "Death From Above (Zerg)", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_death_from_above", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
THE_RECKONING = 49, "The Reckoning (Zerg)", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_the_reckoning", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.HasRaceSwap
# Prologue
DARK_WHISPERS = 50, "Dark Whispers (Protoss)", SC2Campaign.PROLOGUE, "_1", SC2Race.PROTOSS, MissionPools.EASY, "ap_dark_whispers", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsTZ|MissionFlag.HasRaceSwap
GHOSTS_IN_THE_FOG = 51, "Ghosts in the Fog (Protoss)", SC2Campaign.PROLOGUE, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_ghosts_in_the_fog", MissionFlag.Protoss|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
EVIL_AWOKEN = 52, "Evil Awoken", SC2Campaign.PROLOGUE, "_3", SC2Race.PROTOSS, MissionPools.STARTER, "ap_evil_awoken", MissionFlag.Protoss|MissionFlag.NoBuild|MissionFlag.VsProtoss
# LotV
FOR_AIUR = 53, "For Aiur!", SC2Campaign.LOTV, "Aiur", SC2Race.ANY, MissionPools.STARTER, "ap_for_aiur", MissionFlag.Protoss|MissionFlag.NoBuild|MissionFlag.VsZerg
THE_GROWING_SHADOW = 54, "The Growing Shadow (Protoss)", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_growing_shadow", MissionFlag.Protoss|MissionFlag.VsPZ|MissionFlag.HasRaceSwap
THE_SPEAR_OF_ADUN = 55, "The Spear of Adun (Protoss)", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_spear_of_adun", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsPZ|MissionFlag.HasRaceSwap
SKY_SHIELD = 56, "Sky Shield (Protoss)", SC2Campaign.LOTV, "Korhal", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_sky_shield", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.HasRaceSwap
BROTHERS_IN_ARMS = 57, "Brothers in Arms (Protoss)", SC2Campaign.LOTV, "Korhal", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_brothers_in_arms", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.HasRaceSwap
AMON_S_REACH = 58, "Amon's Reach (Protoss)", SC2Campaign.LOTV, "Shakuras", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_amon_s_reach", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
LAST_STAND = 59, "Last Stand (Protoss)", SC2Campaign.LOTV, "Shakuras", SC2Race.PROTOSS, MissionPools.HARD, "ap_last_stand", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
FORBIDDEN_WEAPON = 60, "Forbidden Weapon (Protoss)", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_forbidden_weapon", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
TEMPLE_OF_UNIFICATION = 61, "Temple of Unification (Protoss)", SC2Campaign.LOTV, "Ulnar", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_temple_of_unification", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsTP|MissionFlag.HasRaceSwap
THE_INFINITE_CYCLE = 62, "The Infinite Cycle", SC2Campaign.LOTV, "Ulnar", SC2Race.ANY, MissionPools.HARD, "ap_the_infinite_cycle", MissionFlag.Protoss|MissionFlag.Kerrigan|MissionFlag.NoBuild|MissionFlag.VsTP
HARBINGER_OF_OBLIVION = 63, "Harbinger of Oblivion (Protoss)", SC2Campaign.LOTV, "Ulnar", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_harbinger_of_oblivion", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.Countdown|MissionFlag.VsTP|MissionFlag.AiZergAlly|MissionFlag.HasRaceSwap
UNSEALING_THE_PAST = 64, "Unsealing the Past (Protoss)", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.HARD, "ap_unsealing_the_past", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
PURIFICATION = 65, "Purification (Protoss)", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.HARD, "ap_purification", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsZerg|MissionFlag.HasRaceSwap
STEPS_OF_THE_RITE = 66, "Steps of the Rite (Protoss)", SC2Campaign.LOTV, "Tal'darim", SC2Race.PROTOSS, MissionPools.HARD, "ap_steps_of_the_rite", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
RAK_SHIR = 67, "Rak'Shir (Protoss)", SC2Campaign.LOTV, "Tal'darim", SC2Race.PROTOSS, MissionPools.HARD, "ap_rak_shir", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap
TEMPLAR_S_CHARGE = 68, "Templar's Charge (Protoss)", SC2Campaign.LOTV, "Moebius", SC2Race.PROTOSS, MissionPools.HARD, "ap_templar_s_charge", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsTerran|MissionFlag.HasRaceSwap
TEMPLAR_S_RETURN = 69, "Templar's Return", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_templar_s_return", MissionFlag.Protoss|MissionFlag.NoBuild|MissionFlag.VsPZ
THE_HOST = 70, "The Host (Protoss)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_the_host", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsAll|MissionFlag.HasRaceSwap
SALVATION = 71, "Salvation (Protoss)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_salvation", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.TimedDefense|MissionFlag.VsPZ|MissionFlag.AiProtossAlly|MissionFlag.HasRaceSwap
# Epilogue
INTO_THE_VOID = 72, "Into the Void", SC2Campaign.EPILOGUE, "_1", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_into_the_void", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsAll|MissionFlag.AiTerranAlly|MissionFlag.AiZergAlly
THE_ESSENCE_OF_ETERNITY = 73, "The Essence of Eternity", SC2Campaign.EPILOGUE, "_2", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_the_essence_of_eternity", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsAll|MissionFlag.AiZergAlly|MissionFlag.AiProtossAlly
AMON_S_FALL = 74, "Amon's Fall", SC2Campaign.EPILOGUE, "_3", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_amon_s_fall", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsAll|MissionFlag.AiTerranAlly|MissionFlag.AiProtossAlly
# Nova Covert Ops
THE_ESCAPE = 75, "The Escape", SC2Campaign.NCO, "_1", SC2Race.ANY, MissionPools.MEDIUM, "ap_the_escape", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.NoBuild|MissionFlag.VsTerran
SUDDEN_STRIKE = 76, "Sudden Strike", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_sudden_strike", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.TimedDefense|MissionFlag.VsZerg
ENEMY_INTELLIGENCE = 77, "Enemy Intelligence", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_enemy_intelligence", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.Defense|MissionFlag.VsZerg
TROUBLE_IN_PARADISE = 78, "Trouble In Paradise", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_trouble_in_paradise", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.Countdown|MissionFlag.VsPZ
NIGHT_TERRORS = 79, "Night Terrors", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_night_terrors", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.VsPZ
FLASHPOINT = 80, "Flashpoint", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_flashpoint", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.VsZerg
IN_THE_ENEMY_S_SHADOW = 81, "In the Enemy's Shadow", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_in_the_enemy_s_shadow", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.NoBuild|MissionFlag.VsTerran
DARK_SKIES = 82, "Dark Skies", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.HARD, "ap_dark_skies", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.TimedDefense|MissionFlag.VsProtoss
END_GAME = 83, "End Game", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_end_game", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.Defense|MissionFlag.VsTerran
# Race-Swapped Variants
# 84/85 - Liberation Day
THE_OUTLAWS_Z = 86, "The Outlaws (Zerg)", SC2Campaign.WOL, "Mar Sara", SC2Race.ZERG, MissionPools.EASY, "ap_the_outlaws", MissionFlag.Zerg|MissionFlag.VsTerran|MissionFlag.RaceSwap
THE_OUTLAWS_P = 87, "The Outlaws (Protoss)", SC2Campaign.WOL, "Mar Sara", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_outlaws", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap
ZERO_HOUR_Z = 88, "Zero Hour (Zerg)", SC2Campaign.WOL, "Mar Sara", SC2Race.ZERG, MissionPools.MEDIUM, "ap_zero_hour", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap
ZERO_HOUR_P = 89, "Zero Hour (Protoss)", SC2Campaign.WOL, "Mar Sara", SC2Race.PROTOSS, MissionPools.EASY, "ap_zero_hour", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap
EVACUATION_Z = 90, "Evacuation (Zerg)", SC2Campaign.WOL, "Colonist", SC2Race.ZERG, MissionPools.EASY, "ap_evacuation", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.RaceSwap
EVACUATION_P = 91, "Evacuation (Protoss)", SC2Campaign.WOL, "Colonist", SC2Race.PROTOSS, MissionPools.EASY, "ap_evacuation", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.RaceSwap
OUTBREAK_Z = 92, "Outbreak (Zerg)", SC2Campaign.WOL, "Colonist", SC2Race.ZERG, MissionPools.MEDIUM, "ap_outbreak", MissionFlag.Zerg|MissionFlag.Defense|MissionFlag.VsZerg|MissionFlag.RaceSwap
OUTBREAK_P = 93, "Outbreak (Protoss)", SC2Campaign.WOL, "Colonist", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_outbreak", MissionFlag.Protoss|MissionFlag.Defense|MissionFlag.VsZerg|MissionFlag.RaceSwap
SAFE_HAVEN_Z = 94, "Safe Haven (Zerg)", SC2Campaign.WOL, "Colonist", SC2Race.ZERG, MissionPools.MEDIUM, "ap_safe_haven", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap
SAFE_HAVEN_P = 95, "Safe Haven (Protoss)", SC2Campaign.WOL, "Colonist", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_safe_haven", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap
HAVENS_FALL_Z = 96, "Haven's Fall (Zerg)", SC2Campaign.WOL, "Colonist", SC2Race.ZERG, MissionPools.MEDIUM, "ap_havens_fall", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap
HAVENS_FALL_P = 97, "Haven's Fall (Protoss)", SC2Campaign.WOL, "Colonist", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_havens_fall", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.RaceSwap
SMASH_AND_GRAB_Z = 98, "Smash and Grab (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.EASY, "ap_smash_and_grab", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsPZ|MissionFlag.RaceSwap
SMASH_AND_GRAB_P = 99, "Smash and Grab (Protoss)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.EASY, "ap_smash_and_grab", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsPZ|MissionFlag.RaceSwap
THE_DIG_Z = 100, "The Dig (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_dig", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsProtoss|MissionFlag.RaceSwap
THE_DIG_P = 101, "The Dig (Protoss)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_the_dig", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsProtoss|MissionFlag.RaceSwap
THE_MOEBIUS_FACTOR_Z = 102, "The Moebius Factor (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_moebius_factor", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.RaceSwap
THE_MOEBIUS_FACTOR_P = 103, "The Moebius Factor (Protoss)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_the_moebius_factor", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.RaceSwap
SUPERNOVA_Z = 104, "Supernova (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.HARD, "ap_supernova", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap
SUPERNOVA_P = 105, "Supernova (Protoss)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.HARD, "ap_supernova", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap
MAW_OF_THE_VOID_Z = 106, "Maw of the Void (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.HARD, "ap_maw_of_the_void", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap
MAW_OF_THE_VOID_P = 107, "Maw of the Void (Protoss)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_maw_of_the_void", MissionFlag.Protoss|MissionFlag.VsProtoss|MissionFlag.RaceSwap
DEVILS_PLAYGROUND_Z = 108, "Devil's Playground (Zerg)", SC2Campaign.WOL, "Covert", SC2Race.ZERG, MissionPools.EASY, "ap_devils_playground", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap
DEVILS_PLAYGROUND_P = 109, "Devil's Playground (Protoss)", SC2Campaign.WOL, "Covert", SC2Race.PROTOSS, MissionPools.EASY, "ap_devils_playground", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.RaceSwap
WELCOME_TO_THE_JUNGLE_Z = 110, "Welcome to the Jungle (Zerg)", SC2Campaign.WOL, "Covert", SC2Race.ZERG, MissionPools.HARD, "ap_welcome_to_the_jungle", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap
WELCOME_TO_THE_JUNGLE_P = 111, "Welcome to the Jungle (Protoss)", SC2Campaign.WOL, "Covert", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_welcome_to_the_jungle", MissionFlag.Protoss|MissionFlag.VsProtoss|MissionFlag.RaceSwap
# 112/113 - Breakout
# 114/115 - Ghost of a Chance
THE_GREAT_TRAIN_ROBBERY_Z = 116, "The Great Train Robbery (Zerg)", SC2Campaign.WOL, "Rebellion", SC2Race.ZERG, MissionPools.EASY, "ap_the_great_train_robbery", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap
THE_GREAT_TRAIN_ROBBERY_P = 117, "The Great Train Robbery (Protoss)", SC2Campaign.WOL, "Rebellion", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_great_train_robbery", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap
CUTTHROAT_Z = 118, "Cutthroat (Zerg)", SC2Campaign.WOL, "Rebellion", SC2Race.ZERG, MissionPools.MEDIUM, "ap_cutthroat", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap
CUTTHROAT_P = 119, "Cutthroat (Protoss)", SC2Campaign.WOL, "Rebellion", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_cutthroat", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap
ENGINE_OF_DESTRUCTION_Z = 120, "Engine of Destruction (Zerg)", SC2Campaign.WOL, "Rebellion", SC2Race.ZERG, MissionPools.HARD, "ap_engine_of_destruction", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap
ENGINE_OF_DESTRUCTION_P = 121, "Engine of Destruction (Protoss)", SC2Campaign.WOL, "Rebellion", SC2Race.PROTOSS, MissionPools.HARD, "ap_engine_of_destruction", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap
MEDIA_BLITZ_Z = 122, "Media Blitz (Zerg)", SC2Campaign.WOL, "Rebellion", SC2Race.ZERG, MissionPools.HARD, "ap_media_blitz", MissionFlag.Zerg|MissionFlag.VsTerran|MissionFlag.RaceSwap
MEDIA_BLITZ_P = 123, "Media Blitz (Protoss)", SC2Campaign.WOL, "Rebellion", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_media_blitz", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap
# 124/125 - Piercing the Shroud
# 126/127 - Whispers of Doom
A_SINISTER_TURN_T = 128, "A Sinister Turn (Terran)", SC2Campaign.PROPHECY, "_2", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_a_sinister_turn", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.RaceSwap
A_SINISTER_TURN_Z = 129, "A Sinister Turn (Zerg)", SC2Campaign.PROPHECY, "_2", SC2Race.ZERG, MissionPools.MEDIUM, "ap_a_sinister_turn", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap
ECHOES_OF_THE_FUTURE_T = 130, "Echoes of the Future (Terran)", SC2Campaign.PROPHECY, "_3", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_echoes_of_the_future", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.RaceSwap
ECHOES_OF_THE_FUTURE_Z = 131, "Echoes of the Future (Zerg)", SC2Campaign.PROPHECY, "_3", SC2Race.ZERG, MissionPools.MEDIUM, "ap_echoes_of_the_future", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap
IN_UTTER_DARKNESS_T = 132, "In Utter Darkness (Terran)", SC2Campaign.PROPHECY, "_4", SC2Race.TERRAN, MissionPools.HARD, "ap_in_utter_darkness", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap
IN_UTTER_DARKNESS_Z = 133, "In Utter Darkness (Zerg)", SC2Campaign.PROPHECY, "_4", SC2Race.ZERG, MissionPools.HARD, "ap_in_utter_darkness", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap
GATES_OF_HELL_Z = 134, "Gates of Hell (Zerg)", SC2Campaign.WOL, "Char", SC2Race.ZERG, MissionPools.HARD, "ap_gates_of_hell", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap
GATES_OF_HELL_P = 135, "Gates of Hell (Protoss)", SC2Campaign.WOL, "Char", SC2Race.PROTOSS, MissionPools.HARD, "ap_gates_of_hell", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.RaceSwap
# 136/137 - Belly of the Beast
SHATTER_THE_SKY_Z = 138, "Shatter the Sky (Zerg)", SC2Campaign.WOL, "Char", SC2Race.ZERG, MissionPools.HARD, "ap_shatter_the_sky", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap
SHATTER_THE_SKY_P = 139, "Shatter the Sky (Protoss)", SC2Campaign.WOL, "Char", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_shatter_the_sky", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.RaceSwap
ALL_IN_Z = 140, "All-In (Zerg)", SC2Campaign.WOL, "Char", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_all_in", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap
ALL_IN_P = 141, "All-In (Protoss)", SC2Campaign.WOL, "Char", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_all_in", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap
LAB_RAT_T = 142, "Lab Rat (Terran)", SC2Campaign.HOTS, "Umoja", SC2Race.TERRAN, MissionPools.STARTER, "ap_lab_rat", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.RaceSwap
LAB_RAT_P = 143, "Lab Rat (Protoss)", SC2Campaign.HOTS, "Umoja", SC2Race.PROTOSS, MissionPools.STARTER, "ap_lab_rat", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap
# 144/145 - Back in the Saddle
RENDEZVOUS_T = 146, "Rendezvous (Terran)", SC2Campaign.HOTS, "Umoja", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_rendezvous", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap
RENDEZVOUS_P = 147, "Rendezvous (Protoss)", SC2Campaign.HOTS, "Umoja", SC2Race.PROTOSS, MissionPools.EASY, "ap_rendezvous", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap
HARVEST_OF_SCREAMS_T = 148, "Harvest of Screams (Terran)", SC2Campaign.HOTS, "Kaldir", SC2Race.TERRAN, MissionPools.EASY, "ap_harvest_of_screams", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.RaceSwap
HARVEST_OF_SCREAMS_P = 149, "Harvest of Screams (Protoss)", SC2Campaign.HOTS, "Kaldir", SC2Race.PROTOSS, MissionPools.EASY, "ap_harvest_of_screams", MissionFlag.Protoss|MissionFlag.VsProtoss|MissionFlag.RaceSwap
SHOOT_THE_MESSENGER_T = 150, "Shoot the Messenger (Terran)", SC2Campaign.HOTS, "Kaldir", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_shoot_the_messenger", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap
SHOOT_THE_MESSENGER_P = 151, "Shoot the Messenger (Protoss)", SC2Campaign.HOTS, "Kaldir", SC2Race.PROTOSS, MissionPools.EASY, "ap_shoot_the_messenger", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap
# 152/153 - Enemy Within
DOMINATION_T = 154, "Domination (Terran)", SC2Campaign.HOTS, "Char", SC2Race.TERRAN, MissionPools.EASY, "ap_domination", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.RaceSwap
DOMINATION_P = 155, "Domination (Protoss)", SC2Campaign.HOTS, "Char", SC2Race.PROTOSS, MissionPools.EASY, "ap_domination", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.RaceSwap
FIRE_IN_THE_SKY_T = 156, "Fire in the Sky (Terran)", SC2Campaign.HOTS, "Char", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_fire_in_the_sky", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap
FIRE_IN_THE_SKY_P = 157, "Fire in the Sky (Protoss)", SC2Campaign.HOTS, "Char", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_fire_in_the_sky", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap
OLD_SOLDIERS_T = 158, "Old Soldiers (Terran)", SC2Campaign.HOTS, "Char", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_old_soldiers", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.RaceSwap
OLD_SOLDIERS_P = 159, "Old Soldiers (Protoss)", SC2Campaign.HOTS, "Char", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_old_soldiers", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap
WAKING_THE_ANCIENT_T = 160, "Waking the Ancient (Terran)", SC2Campaign.HOTS, "Zerus", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_waking_the_ancient", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.RaceSwap
WAKING_THE_ANCIENT_P = 161, "Waking the Ancient (Protoss)", SC2Campaign.HOTS, "Zerus", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_waking_the_ancient", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.RaceSwap
THE_CRUCIBLE_T = 162, "The Crucible (Terran)", SC2Campaign.HOTS, "Zerus", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_crucible", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap
THE_CRUCIBLE_P = 163, "The Crucible (Protoss)", SC2Campaign.HOTS, "Zerus", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_the_crucible", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap
# 164/165 - Supreme
INFESTED_T = 166, "Infested (Terran)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_infested", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.RaceSwap
INFESTED_P = 167, "Infested (Protoss)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_infested", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap
HAND_OF_DARKNESS_T = 168, "Hand of Darkness (Terran)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.TERRAN, MissionPools.HARD, "ap_hand_of_darkness", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap
HAND_OF_DARKNESS_P = 169, "Hand of Darkness (Protoss)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.PROTOSS, MissionPools.HARD, "ap_hand_of_darkness", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap
PHANTOMS_OF_THE_VOID_T = 170, "Phantoms of the Void (Terran)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_phantoms_of_the_void", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap
PHANTOMS_OF_THE_VOID_P = 171, "Phantoms of the Void (Protoss)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_phantoms_of_the_void", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap
# 172/173 - With Friends Like These
# 174/175 - Conviction
PLANETFALL_T = 176, "Planetfall (Terran)", SC2Campaign.HOTS, "Korhal", SC2Race.TERRAN, MissionPools.HARD, "ap_planetfall", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap
PLANETFALL_P = 177, "Planetfall (Protoss)", SC2Campaign.HOTS, "Korhal", SC2Race.PROTOSS, MissionPools.HARD, "ap_planetfall", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap
DEATH_FROM_ABOVE_T = 178, "Death From Above (Terran)", SC2Campaign.HOTS, "Korhal", SC2Race.TERRAN, MissionPools.HARD, "ap_death_from_above", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.RaceSwap
DEATH_FROM_ABOVE_P = 179, "Death From Above (Protoss)", SC2Campaign.HOTS, "Korhal", SC2Race.PROTOSS, MissionPools.HARD, "ap_death_from_above", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap
THE_RECKONING_T = 180, "The Reckoning (Terran)", SC2Campaign.HOTS, "Korhal", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_the_reckoning", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap
THE_RECKONING_P = 181, "The Reckoning (Protoss)", SC2Campaign.HOTS, "Korhal", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_the_reckoning", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap
DARK_WHISPERS_T = 182, "Dark Whispers (Terran)", SC2Campaign.PROLOGUE, "_1", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_dark_whispers", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTZ|MissionFlag.RaceSwap
DARK_WHISPERS_Z = 183, "Dark Whispers (Zerg)", SC2Campaign.PROLOGUE, "_1", SC2Race.ZERG, MissionPools.MEDIUM, "ap_dark_whispers", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsTZ|MissionFlag.RaceSwap
GHOSTS_IN_THE_FOG_T = 184, "Ghosts in the Fog (Terran)", SC2Campaign.PROLOGUE, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_ghosts_in_the_fog", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.RaceSwap
GHOSTS_IN_THE_FOG_Z = 185, "Ghosts in the Fog (Zerg)", SC2Campaign.PROLOGUE, "_2", SC2Race.ZERG, MissionPools.HARD, "ap_ghosts_in_the_fog", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap
# 186/187 - Evil Awoken
# 188/189 - For Aiur!
THE_GROWING_SHADOW_T = 190, "The Growing Shadow (Terran)", SC2Campaign.LOTV, "Aiur", SC2Race.TERRAN, MissionPools.EASY, "ap_the_growing_shadow", MissionFlag.Terran|MissionFlag.VsPZ|MissionFlag.RaceSwap
THE_GROWING_SHADOW_Z = 191, "The Growing Shadow (Zerg)", SC2Campaign.LOTV, "Aiur", SC2Race.ZERG, MissionPools.EASY, "ap_the_growing_shadow", MissionFlag.Zerg|MissionFlag.VsPZ|MissionFlag.RaceSwap
THE_SPEAR_OF_ADUN_T = 192, "The Spear of Adun (Terran)", SC2Campaign.LOTV, "Aiur", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_spear_of_adun", MissionFlag.Terran|MissionFlag.VsPZ|MissionFlag.RaceSwap
THE_SPEAR_OF_ADUN_Z = 193, "The Spear of Adun (Zerg)", SC2Campaign.LOTV, "Aiur", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_spear_of_adun", MissionFlag.Zerg|MissionFlag.VsPZ|MissionFlag.RaceSwap
SKY_SHIELD_T = 194, "Sky Shield (Terran)", SC2Campaign.LOTV, "Korhal", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_sky_shield", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap
SKY_SHIELD_Z = 195, "Sky Shield (Zerg)", SC2Campaign.LOTV, "Korhal", SC2Race.ZERG, MissionPools.MEDIUM, "ap_sky_shield", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap
BROTHERS_IN_ARMS_T = 196, "Brothers in Arms (Terran)", SC2Campaign.LOTV, "Korhal", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_brothers_in_arms", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap
BROTHERS_IN_ARMS_Z = 197, "Brothers in Arms (Zerg)", SC2Campaign.LOTV, "Korhal", SC2Race.ZERG, MissionPools.MEDIUM, "ap_brothers_in_arms", MissionFlag.Zerg|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap
AMON_S_REACH_T = 198, "Amon's Reach (Terran)", SC2Campaign.LOTV, "Shakuras", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_amon_s_reach", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.RaceSwap
AMON_S_REACH_Z = 199, "Amon's Reach (Zerg)", SC2Campaign.LOTV, "Shakuras", SC2Race.ZERG, MissionPools.MEDIUM, "ap_amon_s_reach", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap
LAST_STAND_T = 200, "Last Stand (Terran)", SC2Campaign.LOTV, "Shakuras", SC2Race.TERRAN, MissionPools.HARD, "ap_last_stand", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap
LAST_STAND_Z = 201, "Last Stand (Zerg)", SC2Campaign.LOTV, "Shakuras", SC2Race.ZERG, MissionPools.HARD, "ap_last_stand", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap
FORBIDDEN_WEAPON_T = 202, "Forbidden Weapon (Terran)", SC2Campaign.LOTV, "Purifier", SC2Race.TERRAN, MissionPools.HARD, "ap_forbidden_weapon", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap
FORBIDDEN_WEAPON_Z = 203, "Forbidden Weapon (Zerg)", SC2Campaign.LOTV, "Purifier", SC2Race.ZERG, MissionPools.HARD, "ap_forbidden_weapon", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap
TEMPLE_OF_UNIFICATION_T = 204, "Temple of Unification (Terran)", SC2Campaign.LOTV, "Ulnar", SC2Race.TERRAN, MissionPools.HARD, "ap_temple_of_unification", MissionFlag.Terran|MissionFlag.VsTP|MissionFlag.RaceSwap
TEMPLE_OF_UNIFICATION_Z = 205, "Temple of Unification (Zerg)", SC2Campaign.LOTV, "Ulnar", SC2Race.ZERG, MissionPools.HARD, "ap_temple_of_unification", MissionFlag.Zerg|MissionFlag.VsTP|MissionFlag.RaceSwap
# 206/207 - The Infinite Cycle
HARBINGER_OF_OBLIVION_T = 208, "Harbinger of Oblivion (Terran)", SC2Campaign.LOTV, "Ulnar", SC2Race.TERRAN, MissionPools.HARD, "ap_harbinger_of_oblivion", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTP|MissionFlag.AiZergAlly|MissionFlag.RaceSwap
HARBINGER_OF_OBLIVION_Z = 209, "Harbinger of Oblivion (Zerg)", SC2Campaign.LOTV, "Ulnar", SC2Race.ZERG, MissionPools.HARD, "ap_harbinger_of_oblivion", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsTP|MissionFlag.AiZergAlly|MissionFlag.RaceSwap
UNSEALING_THE_PAST_T = 210, "Unsealing the Past (Terran)", SC2Campaign.LOTV, "Purifier", SC2Race.TERRAN, MissionPools.HARD, "ap_unsealing_the_past", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.RaceSwap
UNSEALING_THE_PAST_Z = 211, "Unsealing the Past (Zerg)", SC2Campaign.LOTV, "Purifier", SC2Race.ZERG, MissionPools.HARD, "ap_unsealing_the_past", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.RaceSwap
PURIFICATION_T = 212, "Purification (Terran)", SC2Campaign.LOTV, "Purifier", SC2Race.TERRAN, MissionPools.HARD, "ap_purification", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.RaceSwap
PURIFICATION_Z = 213, "Purification (Zerg)", SC2Campaign.LOTV, "Purifier", SC2Race.ZERG, MissionPools.HARD, "ap_purification", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap
STEPS_OF_THE_RITE_T = 214, "Steps of the Rite (Terran)", SC2Campaign.LOTV, "Tal'darim", SC2Race.TERRAN, MissionPools.HARD, "ap_steps_of_the_rite", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.RaceSwap
STEPS_OF_THE_RITE_Z = 215, "Steps of the Rite (Zerg)", SC2Campaign.LOTV, "Tal'darim", SC2Race.ZERG, MissionPools.HARD, "ap_steps_of_the_rite", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap
RAK_SHIR_T = 216, "Rak'Shir (Terran)", SC2Campaign.LOTV, "Tal'darim", SC2Race.TERRAN, MissionPools.HARD, "ap_rak_shir", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.RaceSwap
RAK_SHIR_Z = 217, "Rak'Shir (Zerg)", SC2Campaign.LOTV, "Tal'darim", SC2Race.ZERG, MissionPools.HARD, "ap_rak_shir", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap
TEMPLAR_S_CHARGE_T = 218, "Templar's Charge (Terran)", SC2Campaign.LOTV, "Moebius", SC2Race.TERRAN, MissionPools.HARD, "ap_templar_s_charge", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.RaceSwap
TEMPLAR_S_CHARGE_Z = 219, "Templar's Charge (Zerg)", SC2Campaign.LOTV, "Moebius", SC2Race.ZERG, MissionPools.HARD, "ap_templar_s_charge", MissionFlag.Zerg|MissionFlag.VsTerran|MissionFlag.RaceSwap
# 220/221 - Templar's Return
THE_HOST_T = 222, "The Host (Terran)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_the_host", MissionFlag.Terran|MissionFlag.VsAll|MissionFlag.RaceSwap
THE_HOST_Z = 223, "The Host (Zerg)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_the_host", MissionFlag.Zerg|MissionFlag.VsAll|MissionFlag.RaceSwap
SALVATION_T = 224, "Salvation (Terran)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_salvation", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsPZ|MissionFlag.AiProtossAlly|MissionFlag.RaceSwap
SALVATION_Z = 225, "Salvation (Zerg)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_salvation", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsPZ|MissionFlag.AiProtossAlly|MissionFlag.RaceSwap
# 226/227 - Into the Void
# 228/229 - The Essence of Eternity
# 230/231 - Amon's Fall
# 232/233 - The Escape
# 234/235 - Sudden Strike
# 236/237 - Enemy Intelligence
# 238/239 - Trouble In Paradise
# 240/241 - Night Terrors
# 242/243 - Flashpoint
# 244/245 - In the Enemy's Shadow
# 246/247 - Dark Skies
# 248/249 - End Game
class MissionConnection:
campaign: SC2Campaign
connect_to: int # -1 connects to Menu
def __init__(self, connect_to, campaign = SC2Campaign.GLOBAL):
self.campaign = campaign
self.connect_to = connect_to
def _asdict(self):
return {
"campaign": self.campaign.id,
"connect_to": self.connect_to
}
class MissionInfo(NamedTuple):
mission: SC2Mission
required_world: List[Union[MissionConnection, Dict[Literal["campaign", "connect_to"], int]]]
category: str
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
ui_vertical_padding: int = 0 # How many blank padding tiles go above this mission in the launcher
lookup_id_to_mission: Dict[int, SC2Mission] = {
mission.id: mission for mission in SC2Mission
}
lookup_name_to_mission: Dict[str, SC2Mission] = {
mission.mission_name: mission for mission in SC2Mission
}
for mission in SC2Mission:
if MissionFlag.HasRaceSwap in mission.flags and ' (' in mission.mission_name:
# Short names for non-race-swapped missions for client compatibility
short_name = mission.get_short_name()
lookup_name_to_mission[short_name] = mission
lookup_id_to_campaign: Dict[int, SC2Campaign] = {
campaign.id: campaign for campaign in SC2Campaign
}
campaign_mission_table: Dict[SC2Campaign, Set[SC2Mission]] = {
campaign: set() for campaign in SC2Campaign
}
for mission in SC2Mission:
campaign_mission_table[mission.campaign].add(mission)
def get_campaign_difficulty(campaign: SC2Campaign, excluded_missions: Iterable[SC2Mission] = ()) -> MissionPools:
"""
:param campaign:
:param excluded_missions:
:return: Campaign's the most difficult non-excluded mission
"""
excluded_mission_set = set(excluded_missions)
included_missions = campaign_mission_table[campaign].difference(excluded_mission_set)
return max([mission.pool for mission in included_missions])
def get_campaign_goal_priority(campaign: SC2Campaign, excluded_missions: Iterable[SC2Mission] = ()) -> SC2CampaignGoalPriority:
"""
Gets a modified campaign goal priority.
If all the campaign's goal missions are excluded, it's ineligible to have the goal
If the campaign's very hard missions are excluded, the priority is lowered to hard
:param campaign:
:param excluded_missions:
:return:
"""
if excluded_missions is None:
return campaign.goal_priority
else:
goal_missions = set(get_campaign_potential_goal_missions(campaign))
excluded_mission_set = set(excluded_missions)
remaining_goals = goal_missions.difference(excluded_mission_set)
if remaining_goals == set():
# All potential goals are excluded, the campaign can't be a goal
return SC2CampaignGoalPriority.NONE
elif campaign.goal_priority == SC2CampaignGoalPriority.VERY_HARD:
# Check if a very hard campaign doesn't get rid of it's last very hard mission
difficulty = get_campaign_difficulty(campaign, excluded_missions)
if difficulty == MissionPools.VERY_HARD:
return SC2CampaignGoalPriority.VERY_HARD
else:
return SC2CampaignGoalPriority.HARD
else:
return campaign.goal_priority
class SC2CampaignGoal(NamedTuple):
mission: SC2Mission
location: str
campaign_final_mission_locations: Dict[SC2Campaign, Optional[SC2CampaignGoal]] = {
SC2Campaign.WOL: SC2CampaignGoal(SC2Mission.ALL_IN, f'{SC2Mission.ALL_IN.mission_name}: Victory'),
SC2Campaign.PROPHECY: SC2CampaignGoal(SC2Mission.IN_UTTER_DARKNESS, f'{SC2Mission.IN_UTTER_DARKNESS.mission_name}: Defeat'),
SC2Campaign.HOTS: SC2CampaignGoal(SC2Mission.THE_RECKONING, f'{SC2Mission.THE_RECKONING.mission_name}: Victory'),
SC2Campaign.PROLOGUE: SC2CampaignGoal(SC2Mission.EVIL_AWOKEN, f'{SC2Mission.EVIL_AWOKEN.mission_name}: Victory'),
SC2Campaign.LOTV: SC2CampaignGoal(SC2Mission.SALVATION, f'{SC2Mission.SALVATION.mission_name}: Victory'),
SC2Campaign.EPILOGUE: None,
SC2Campaign.NCO: SC2CampaignGoal(SC2Mission.END_GAME, f'{SC2Mission.END_GAME.mission_name}: Victory'),
}
campaign_alt_final_mission_locations: Dict[SC2Campaign, Dict[SC2Mission, str]] = {
SC2Campaign.WOL: {
SC2Mission.MAW_OF_THE_VOID: f'{SC2Mission.MAW_OF_THE_VOID.mission_name}: Victory',
SC2Mission.ENGINE_OF_DESTRUCTION: f'{SC2Mission.ENGINE_OF_DESTRUCTION.mission_name}: Victory',
SC2Mission.SUPERNOVA: f'{SC2Mission.SUPERNOVA.mission_name}: Victory',
SC2Mission.GATES_OF_HELL: f'{SC2Mission.GATES_OF_HELL.mission_name}: Victory',
SC2Mission.SHATTER_THE_SKY: f'{SC2Mission.SHATTER_THE_SKY.mission_name}: Victory',
SC2Mission.MAW_OF_THE_VOID_Z: f'{SC2Mission.MAW_OF_THE_VOID_Z.mission_name}: Victory',
SC2Mission.ENGINE_OF_DESTRUCTION_Z: f'{SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name}: Victory',
SC2Mission.SUPERNOVA_Z: f'{SC2Mission.SUPERNOVA_Z.mission_name}: Victory',
SC2Mission.GATES_OF_HELL_Z: f'{SC2Mission.GATES_OF_HELL_Z.mission_name}: Victory',
SC2Mission.SHATTER_THE_SKY_Z: f'{SC2Mission.SHATTER_THE_SKY_Z.mission_name}: Victory',
SC2Mission.MAW_OF_THE_VOID_P: f'{SC2Mission.MAW_OF_THE_VOID_P.mission_name}: Victory',
SC2Mission.ENGINE_OF_DESTRUCTION_P: f'{SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name}: Victory',
SC2Mission.SUPERNOVA_P: f'{SC2Mission.SUPERNOVA_P.mission_name}: Victory',
SC2Mission.GATES_OF_HELL_P: f'{SC2Mission.GATES_OF_HELL_P.mission_name}: Victory',
SC2Mission.SHATTER_THE_SKY_P: f'{SC2Mission.SHATTER_THE_SKY_P.mission_name}: Victory'
},
SC2Campaign.PROPHECY: {},
SC2Campaign.HOTS: {
SC2Mission.THE_CRUCIBLE: f'{SC2Mission.THE_CRUCIBLE.mission_name}: Victory',
SC2Mission.HAND_OF_DARKNESS: f'{SC2Mission.HAND_OF_DARKNESS.mission_name}: Victory',
SC2Mission.PHANTOMS_OF_THE_VOID: f'{SC2Mission.PHANTOMS_OF_THE_VOID.mission_name}: Victory',
SC2Mission.PLANETFALL: f'{SC2Mission.PLANETFALL.mission_name}: Victory',
SC2Mission.DEATH_FROM_ABOVE: f'{SC2Mission.DEATH_FROM_ABOVE.mission_name}: Victory',
SC2Mission.THE_CRUCIBLE_T: f'{SC2Mission.THE_CRUCIBLE_T.mission_name}: Victory',
SC2Mission.HAND_OF_DARKNESS_T: f'{SC2Mission.HAND_OF_DARKNESS_T.mission_name}: Victory',
SC2Mission.PHANTOMS_OF_THE_VOID_T: f'{SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name}: Victory',
SC2Mission.PLANETFALL_T: f'{SC2Mission.PLANETFALL_T.mission_name}: Victory',
SC2Mission.DEATH_FROM_ABOVE_T: f'{SC2Mission.DEATH_FROM_ABOVE_T.mission_name}: Victory',
SC2Mission.THE_CRUCIBLE_P: f'{SC2Mission.THE_CRUCIBLE_P.mission_name}: Victory',
SC2Mission.HAND_OF_DARKNESS_P: f'{SC2Mission.HAND_OF_DARKNESS_P.mission_name}: Victory',
SC2Mission.PHANTOMS_OF_THE_VOID_P: f'{SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name}: Victory',
SC2Mission.PLANETFALL_P: f'{SC2Mission.PLANETFALL_P.mission_name}: Victory',
SC2Mission.DEATH_FROM_ABOVE_P: f'{SC2Mission.DEATH_FROM_ABOVE_P.mission_name}: Victory'
},
SC2Campaign.PROLOGUE: {
SC2Mission.GHOSTS_IN_THE_FOG: f'{SC2Mission.GHOSTS_IN_THE_FOG.mission_name}: Victory',
SC2Mission.GHOSTS_IN_THE_FOG_T: f'{SC2Mission.GHOSTS_IN_THE_FOG_T.mission_name}: Victory',
SC2Mission.GHOSTS_IN_THE_FOG_Z: f'{SC2Mission.GHOSTS_IN_THE_FOG_Z.mission_name}: Victory'
},
SC2Campaign.LOTV: {
SC2Mission.THE_HOST: f'{SC2Mission.THE_HOST.mission_name}: Victory',
SC2Mission.TEMPLAR_S_CHARGE: f'{SC2Mission.TEMPLAR_S_CHARGE.mission_name}: Victory',
SC2Mission.THE_HOST_T: f'{SC2Mission.THE_HOST_T.mission_name}: Victory',
SC2Mission.TEMPLAR_S_CHARGE_T: f'{SC2Mission.TEMPLAR_S_CHARGE_T.mission_name}: Victory',
SC2Mission.THE_HOST_Z: f'{SC2Mission.THE_HOST_Z.mission_name}: Victory',
SC2Mission.TEMPLAR_S_CHARGE_Z: f'{SC2Mission.TEMPLAR_S_CHARGE_Z.mission_name}: Victory'
},
SC2Campaign.EPILOGUE: {
SC2Mission.AMON_S_FALL: f'{SC2Mission.AMON_S_FALL.mission_name}: Victory',
SC2Mission.INTO_THE_VOID: f'{SC2Mission.INTO_THE_VOID.mission_name}: Victory',
SC2Mission.THE_ESSENCE_OF_ETERNITY: f'{SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name}: Victory',
},
SC2Campaign.NCO: {
SC2Mission.FLASHPOINT: f'{SC2Mission.FLASHPOINT.mission_name}: Victory',
SC2Mission.DARK_SKIES: f'{SC2Mission.DARK_SKIES.mission_name}: Victory',
SC2Mission.NIGHT_TERRORS: f'{SC2Mission.NIGHT_TERRORS.mission_name}: Victory',
SC2Mission.TROUBLE_IN_PARADISE: f'{SC2Mission.TROUBLE_IN_PARADISE.mission_name}: Victory'
}
}
campaign_race_exceptions: Dict[SC2Mission, SC2Race] = {
SC2Mission.WITH_FRIENDS_LIKE_THESE: SC2Race.TERRAN
}
def get_goal_location(mission: SC2Mission) -> Union[str, None]:
"""
:param mission:
:return: Goal location assigned to the goal mission
"""
campaign = mission.campaign
primary_campaign_goal = campaign_final_mission_locations[campaign]
if primary_campaign_goal is not None:
if primary_campaign_goal.mission == mission:
return primary_campaign_goal.location
campaign_alt_goals = campaign_alt_final_mission_locations[campaign]
if mission in campaign_alt_goals:
return campaign_alt_goals.get(mission)
return (mission.mission_name + ": Defeat") \
if mission in [SC2Mission.IN_UTTER_DARKNESS, SC2Mission.IN_UTTER_DARKNESS_T, SC2Mission.IN_UTTER_DARKNESS_Z] \
else mission.mission_name + ": Victory"
def get_campaign_potential_goal_missions(campaign: SC2Campaign) -> List[SC2Mission]:
"""
:param campaign:
:return: All missions that can be the campaign's goal
"""
missions: List[SC2Mission] = list()
primary_goal_mission = campaign_final_mission_locations[campaign]
if primary_goal_mission is not None:
missions.append(primary_goal_mission.mission)
alt_goal_locations = campaign_alt_final_mission_locations[campaign]
if alt_goal_locations:
for mission in alt_goal_locations.keys():
missions.append(mission)
return missions
def get_missions_with_any_flags_in_list(flags: MissionFlag) -> List[SC2Mission]:
return [mission for mission in SC2Mission if flags & mission.flags]

1746
worlds/sc2/options.py Normal file

File diff suppressed because it is too large Load Diff

493
worlds/sc2/pool_filter.py Normal file
View File

@@ -0,0 +1,493 @@
import logging
from typing import Callable, Dict, List, Set, Tuple, TYPE_CHECKING, Iterable
from BaseClasses import Location, ItemClassification
from .item import StarcraftItem, ItemFilterFlags, item_names, item_parents, item_groups
from .item.item_tables import item_table, TerranItemType, ZergItemType, spear_of_adun_calldowns, \
spear_of_adun_castable_passives
from .options import RequiredTactics
if TYPE_CHECKING:
from . import SC2World
# Items that can be placed before resources if not already in
# General upgrades and Mercs
second_pass_placeable_items: Tuple[str, ...] = (
# Global weapon/armor upgrades
item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE,
item_names.PROGRESSIVE_TERRAN_WEAPON_UPGRADE,
item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE,
item_names.PROGRESSIVE_ZERG_ARMOR_UPGRADE,
item_names.PROGRESSIVE_ZERG_WEAPON_UPGRADE,
item_names.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE,
item_names.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE,
item_names.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE,
item_names.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE,
item_names.PROGRESSIVE_PROTOSS_SHIELDS,
# Terran Buildings without upgrades
item_names.SENSOR_TOWER,
item_names.HIVE_MIND_EMULATOR,
item_names.PSI_DISRUPTER,
item_names.PERDITION_TURRET,
# General Terran upgrades without any dependencies
item_names.SCV_ADVANCED_CONSTRUCTION,
item_names.SCV_DUAL_FUSION_WELDERS,
item_names.SCV_CONSTRUCTION_JUMP_JETS,
item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM,
item_names.PROGRESSIVE_ORBITAL_COMMAND,
item_names.ULTRA_CAPACITORS,
item_names.VANADIUM_PLATING,
item_names.ORBITAL_DEPOTS,
item_names.MICRO_FILTERING,
item_names.AUTOMATED_REFINERY,
item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR,
item_names.COMMAND_CENTER_SCANNER_SWEEP,
item_names.COMMAND_CENTER_MULE,
item_names.COMMAND_CENTER_EXTRA_SUPPLIES,
item_names.TECH_REACTOR,
item_names.CELLULAR_REACTOR,
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL, # Place only L1
item_names.STRUCTURE_ARMOR,
item_names.HI_SEC_AUTO_TRACKING,
item_names.ADVANCED_OPTICS,
item_names.ROGUE_FORCES,
# Mercenaries (All races)
*[item_name for item_name, item_data in item_table.items()
if item_data.type in (TerranItemType.Mercenary, ZergItemType.Mercenary)],
# Kerrigan and Nova levels, abilities and generally useful stuff
*[item_name for item_name, item_data in item_table.items()
if item_data.type in (
ZergItemType.Level,
ZergItemType.Ability,
ZergItemType.Evolution_Pit,
TerranItemType.Nova_Gear
)],
item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE,
# Zerg static defenses
item_names.SPORE_CRAWLER,
item_names.SPINE_CRAWLER,
# Overseer
item_names.OVERLORD_OVERSEER_ASPECT,
# Spear of Adun Abilities
item_names.SOA_CHRONO_SURGE,
item_names.SOA_PROGRESSIVE_PROXY_PYLON,
item_names.SOA_PYLON_OVERCHARGE,
item_names.SOA_ORBITAL_STRIKE,
item_names.SOA_TEMPORAL_FIELD,
item_names.SOA_SOLAR_LANCE,
item_names.SOA_MASS_RECALL,
item_names.SOA_SHIELD_OVERCHARGE,
item_names.SOA_DEPLOY_FENIX,
item_names.SOA_PURIFIER_BEAM,
item_names.SOA_TIME_STOP,
item_names.SOA_SOLAR_BOMBARDMENT,
# Protoss generic upgrades
item_names.MATRIX_OVERLOAD,
item_names.QUATRO,
item_names.NEXUS_OVERCHARGE,
item_names.ORBITAL_ASSIMILATORS,
item_names.WARP_HARMONIZATION,
item_names.GUARDIAN_SHELL,
item_names.RECONSTRUCTION_BEAM,
item_names.OVERWATCH,
item_names.SUPERIOR_WARP_GATES,
item_names.KHALAI_INGENUITY,
item_names.AMPLIFIED_ASSIMILATORS,
# Protoss static defenses
item_names.PHOTON_CANNON,
item_names.KHAYDARIN_MONOLITH,
item_names.SHIELD_BATTERY,
)
def copy_item(item: StarcraftItem) -> StarcraftItem:
return StarcraftItem(item.name, item.classification, item.code, item.player, item.filter_flags)
class ValidInventory:
def __init__(self, world: 'SC2World', item_pool: List[StarcraftItem]) -> None:
self.multiworld = world.multiworld
self.player = world.player
self.world: 'SC2World' = world
# Track all Progression items and those with complex rules for filtering
self.logical_inventory: Dict[str, int] = {}
for item in item_pool:
if not item_table[item.name].is_important_for_filtering():
continue
self.logical_inventory.setdefault(item.name, 0)
self.logical_inventory[item.name] += 1
self.item_pool = item_pool
self.item_name_to_item: Dict[str, List[StarcraftItem]] = {}
self.item_name_to_child_items: Dict[str, List[StarcraftItem]] = {}
for item in item_pool:
self.item_name_to_item.setdefault(item.name, []).append(item)
for parent_item in item_parents.child_item_to_parent_items.get(item.name, []):
self.item_name_to_child_items.setdefault(parent_item, []).append(item)
def has(self, item: str, player: int, count: int = 1) -> bool:
return self.logical_inventory.get(item, 0) >= count
def has_any(self, items: Set[str], player: int) -> bool:
return any(self.logical_inventory.get(item) for item in items)
def has_all(self, items: Set[str], player: int) -> bool:
return all(self.logical_inventory.get(item) for item in items)
def has_group(self, item_group: str, player: int, count: int = 1) -> bool:
return False # Deliberately fails here, as item pooling is not aware about mission layout
def count_group(self, item_name_group: str, player: int) -> int:
return 0 # For item filtering assume no missions are beaten
def count(self, item: str, player: int) -> int:
return self.logical_inventory.get(item, 0)
def count_from_list(self, items: Iterable[str], player: int) -> int:
return sum(self.logical_inventory.get(item, 0) for item in items)
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
return sum(item in self.logical_inventory for item in items)
def generate_reduced_inventory(self, inventory_size: int, filler_amount: int, mission_requirements: List[Tuple[str, Callable]]) -> List[StarcraftItem]:
"""Attempts to generate a reduced inventory that can fulfill the mission requirements."""
inventory: List[StarcraftItem] = list(self.item_pool)
requirements = mission_requirements
min_upgrades_per_unit = self.world.options.min_number_of_upgrades.value
max_upgrades_per_unit = self.world.options.max_number_of_upgrades.value
if max_upgrades_per_unit > -1 and min_upgrades_per_unit > max_upgrades_per_unit:
logging.getLogger("Starcraft 2").warning(
f"min upgrades per unit is greater than max upgrades per unit ({min_upgrades_per_unit} > {max_upgrades_per_unit}). "
f"Setting both to minimum value ({min_upgrades_per_unit})"
)
max_upgrades_per_unit = min_upgrades_per_unit
def attempt_removal(
item: StarcraftItem,
remove_flag: ItemFilterFlags = ItemFilterFlags.FilterExcluded,
) -> str:
"""
Returns empty string and applies `remove_flag` if the item is removable,
else returns a string containing failed locations and applies ItemFilterFlags.LogicLocked
"""
# Only run logic checks when removing logic items
if self.logical_inventory.get(item.name, 0) > 0:
self.logical_inventory[item.name] -= 1
failed_rules = [name for name, requirement in mission_requirements if not requirement(self)]
if failed_rules:
# If item cannot be removed, lock and revert
self.logical_inventory[item.name] += 1
item.filter_flags |= ItemFilterFlags.LogicLocked
return f"{len(failed_rules)} rules starting with \"{failed_rules[0]}\""
if not self.logical_inventory[item.name]:
del self.logical_inventory[item.name]
item.filter_flags |= remove_flag
return ""
def remove_child_items(
parent_item: StarcraftItem,
remove_flag: ItemFilterFlags = ItemFilterFlags.FilterExcluded,
) -> None:
child_items = self.item_name_to_child_items.get(parent_item.name, [])
for child_item in child_items:
if (ItemFilterFlags.AllowedOrphan|ItemFilterFlags.Unexcludable) & child_item.filter_flags:
continue
parent_id = item_table[child_item.name].parent
assert parent_id is not None
if item_parents.parent_present[parent_id](self.logical_inventory, self.world.options):
continue
if not attempt_removal(child_item, remove_flag):
remove_child_items(child_item, remove_flag)
def cull_items_over_maximum(group: List[StarcraftItem], allowed_max: int) -> None:
for item in group:
if len([x for x in group if ItemFilterFlags.Culled not in x.filter_flags]) <= allowed_max:
break
if ItemFilterFlags.Uncullable & item.filter_flags:
continue
attempt_removal(item, remove_flag=ItemFilterFlags.Culled)
def request_minimum_items(group: List[StarcraftItem], requested_minimum) -> None:
for item in group:
if len([x for x in group if ItemFilterFlags.RequestedOrBetter & x.filter_flags]) >= requested_minimum:
break
if ItemFilterFlags.Culled & item.filter_flags:
continue
item.filter_flags |= ItemFilterFlags.Requested
# Process Excluded items, validate if the item can get actually excluded
excluded_items: List[StarcraftItem] = [starcraft_item for starcraft_item in inventory if ItemFilterFlags.Excluded & starcraft_item.filter_flags]
self.world.random.shuffle(excluded_items)
for excluded_item in excluded_items:
if ItemFilterFlags.Unexcludable & excluded_item.filter_flags:
continue
removal_failed = attempt_removal(excluded_item, remove_flag=ItemFilterFlags.Removed)
if removal_failed:
if ItemFilterFlags.UserExcluded in excluded_item.filter_flags:
logging.getLogger("Starcraft 2").warning(
f"Cannot exclude item {excluded_item.name} as it would break {removal_failed}"
)
else:
assert False, f"Item filtering excluded an item which is logically required: {excluded_item.name}"
continue
remove_child_items(excluded_item, remove_flag=ItemFilterFlags.Removed)
inventory = [item for item in inventory if ItemFilterFlags.Removed not in item.filter_flags]
# Clear excluded flags; all existing ones should be implemented or out-of-logic
for item in inventory:
item.filter_flags &= ~ItemFilterFlags.Excluded
# Determine item groups to be constrained by min/max upgrades per unit
group_to_item: Dict[str, List[StarcraftItem]] = {}
group: str = ""
for group, group_member_names in item_parents.item_upgrade_groups.items():
group_to_item[group] = []
for item_name in group_member_names:
inventory_items = self.item_name_to_item.get(item_name, [])
group_to_item[group].extend(item for item in inventory_items if ItemFilterFlags.Removed not in item.filter_flags)
# Limit the maximum number of upgrades
if max_upgrades_per_unit != -1:
for group_name, group_items in group_to_item.items():
self.world.random.shuffle(group_to_item[group])
cull_items_over_maximum(group_items, max_upgrades_per_unit)
# Requesting minimum upgrades for items that have already been locked/placed when minimum required
if min_upgrades_per_unit != -1:
for group_name, group_items in group_to_item.items():
self.world.random.shuffle(group_items)
request_minimum_items(group_items, min_upgrades_per_unit)
# Kerrigan max abilities
kerrigan_actives = [item for item in inventory if item.name in item_groups.kerrigan_active_abilities]
self.world.random.shuffle(kerrigan_actives)
cull_items_over_maximum(kerrigan_actives, self.world.options.kerrigan_max_active_abilities.value)
kerrigan_passives = [item for item in inventory if item.name in item_groups.kerrigan_passives]
self.world.random.shuffle(kerrigan_passives)
cull_items_over_maximum(kerrigan_passives, self.world.options.kerrigan_max_passive_abilities.value)
# Spear of Adun max abilities
spear_of_adun_actives = [item for item in inventory if item.name in spear_of_adun_calldowns]
self.world.random.shuffle(spear_of_adun_actives)
cull_items_over_maximum(spear_of_adun_actives, self.world.options.spear_of_adun_max_active_abilities.value)
spear_of_adun_autocasts = [item for item in inventory if item.name in spear_of_adun_castable_passives]
self.world.random.shuffle(spear_of_adun_autocasts)
cull_items_over_maximum(spear_of_adun_autocasts, self.world.options.spear_of_adun_max_passive_abilities.value)
# Nova items
nova_weapon_items = [item for item in inventory if item.name in item_groups.nova_weapons]
self.world.random.shuffle(nova_weapon_items)
cull_items_over_maximum(nova_weapon_items, self.world.options.nova_max_weapons.value)
nova_gadget_items = [item for item in inventory if item.name in item_groups.nova_gadgets]
self.world.random.shuffle(nova_gadget_items)
cull_items_over_maximum(nova_gadget_items, self.world.options.nova_max_gadgets.value)
# Determining if the full-size inventory can complete campaign
# Note(mm): Now that user excludes are checked against logic, this can probably never fail unless there's a bug.
failed_locations: List[str] = [location for (location, requirement) in requirements if not requirement(self)]
if len(failed_locations) > 0:
raise Exception(f"Too many items excluded - couldn't satisfy access rules for the following locations:\n{failed_locations}")
# Optionally locking generic items
generic_items: List[StarcraftItem] = [
starcraft_item for starcraft_item in inventory
if starcraft_item.name in second_pass_placeable_items
and (
not ItemFilterFlags.CulledOrBetter & starcraft_item.filter_flags
or ItemFilterFlags.RequestedOrBetter & starcraft_item.filter_flags
)
]
reserved_generic_percent = self.world.options.ensure_generic_items.value / 100
reserved_generic_amount = int(len(generic_items) * reserved_generic_percent)
self.world.random.shuffle(generic_items)
for starcraft_item in generic_items[:reserved_generic_amount]:
starcraft_item.filter_flags |= ItemFilterFlags.Requested
# Main cull process
def remove_random_item(
removable: List[StarcraftItem],
dont_remove_flags: ItemFilterFlags,
remove_flag: ItemFilterFlags = ItemFilterFlags.Removed,
) -> bool:
if len(removable) == 0:
return False
item = self.world.random.choice(removable)
# Do not remove item if it would drop upgrades below minimum
if min_upgrades_per_unit > 0:
group_name = None
parent = item_table[item.name].parent
if parent is not None:
group_name = item_parents.parent_present[parent].constraint_group
if group_name is not None:
children = group_to_item.get(group_name, [])
children = [x for x in children if not (ItemFilterFlags.CulledOrBetter & x.filter_flags)]
if len(children) <= min_upgrades_per_unit:
# Attempt to remove a parent instead, if possible
dont_remove = ItemFilterFlags.Removed|dont_remove_flags
parent_items = [
parent_item
for parent_name in item_parents.child_item_to_parent_items[item.name]
for parent_item in self.item_name_to_item.get(parent_name, [])
if not (dont_remove & parent_item.filter_flags)
]
if parent_items:
item = self.world.random.choice(parent_items)
else:
# Lock remaining upgrades
for item in children:
item.filter_flags |= ItemFilterFlags.Locked
return False
if not attempt_removal(item, remove_flag):
remove_child_items(item, remove_flag)
return True
return False
def item_included(item: StarcraftItem) -> bool:
return bool(
ItemFilterFlags.Removed not in item.filter_flags
and ((ItemFilterFlags.Unexcludable|ItemFilterFlags.Excluded) & item.filter_flags) != ItemFilterFlags.Excluded
)
# Actually remove culled items; we won't re-add them
inventory = [
item for item in inventory
if (((ItemFilterFlags.Uncullable|ItemFilterFlags.Culled) & item.filter_flags) != ItemFilterFlags.Culled)
]
# Part 1: Remove items that are not requested
start_inventory_size = len([item for item in inventory if ItemFilterFlags.StartInventory in item.filter_flags])
current_inventory_size = len([item for item in inventory if item_included(item)])
cullable_items = [item for item in inventory if not (ItemFilterFlags.Uncullable & item.filter_flags)]
while current_inventory_size - start_inventory_size > inventory_size - filler_amount:
if len(cullable_items) == 0:
if filler_amount > 0:
filler_amount -= 1
else:
break
if remove_random_item(cullable_items, ItemFilterFlags.Uncullable):
inventory = [item for item in inventory if ItemFilterFlags.Removed not in item.filter_flags]
current_inventory_size = len([item for item in inventory if item_included(item)])
cullable_items = [
item for item in cullable_items
if not ((ItemFilterFlags.Removed|ItemFilterFlags.Uncullable) & item.filter_flags)
]
# Handle too many requested
if current_inventory_size - start_inventory_size > inventory_size - filler_amount:
for item in inventory:
item.filter_flags &= ~ItemFilterFlags.Requested
# Part 2: If we need to remove more, allow removing requested items
excludable_items = [item for item in inventory if not (ItemFilterFlags.Unexcludable & item.filter_flags)]
while current_inventory_size - start_inventory_size > inventory_size - filler_amount:
if len(excludable_items) == 0:
break
if remove_random_item(excludable_items, ItemFilterFlags.Unexcludable):
inventory = [item for item in inventory if ItemFilterFlags.Removed not in item.filter_flags]
current_inventory_size = len([item for item in inventory if item_included(item)])
excludable_items = [
item for item in inventory
if not ((ItemFilterFlags.Removed|ItemFilterFlags.Unexcludable) & item.filter_flags)
]
# Part 3: If it still doesn't fit, move locked items to start inventory until it fits
precollect_items = current_inventory_size - inventory_size - start_inventory_size - filler_amount
if precollect_items > 0:
promotable = [
item
for item in inventory
if ItemFilterFlags.StartInventory not in item.filter_flags
and ItemFilterFlags.Locked in item.filter_flags
]
self.world.random.shuffle(promotable)
for item in promotable[:precollect_items]:
item.filter_flags |= ItemFilterFlags.StartInventory
start_inventory_size += 1
# Removing extra dependencies
# Transport Hook
if not self.logical_inventory.get(item_names.MEDIVAC):
# Don't allow L2 Siege Tank Transport Hook without Medivac
inventory_transport_hooks = [item for item in inventory if item.name == item_names.SIEGE_TANK_PROGRESSIVE_TRANSPORT_HOOK]
removable_transport_hooks = [item for item in inventory_transport_hooks if not (ItemFilterFlags.Unexcludable & item.filter_flags)]
if len(inventory_transport_hooks) > 1 and removable_transport_hooks:
inventory.remove(removable_transport_hooks[0])
# Weapon/Armour upgrades
def exclude_wa(prefix: str) -> List[StarcraftItem]:
return [
item for item in inventory
if (ItemFilterFlags.UnexcludableUpgrade & item.filter_flags)
or not item.name.startswith(prefix)
]
used_item_names: Set[str] = {item.name for item in inventory}
if used_item_names.isdisjoint(item_groups.barracks_wa_group):
inventory = exclude_wa(item_names.TERRAN_INFANTRY_UPGRADE_PREFIX)
if used_item_names.isdisjoint(item_groups.factory_wa_group):
inventory = exclude_wa(item_names.TERRAN_VEHICLE_UPGRADE_PREFIX)
if used_item_names.isdisjoint(item_groups.starport_wa_group):
inventory = exclude_wa(item_names.TERRAN_SHIP_UPGRADE_PREFIX)
if used_item_names.isdisjoint(item_groups.zerg_melee_wa):
inventory = exclude_wa(item_names.PROGRESSIVE_ZERG_MELEE_ATTACK)
if used_item_names.isdisjoint(item_groups.zerg_ranged_wa):
inventory = exclude_wa(item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK)
if used_item_names.isdisjoint(item_groups.zerg_air_units):
inventory = exclude_wa(item_names.ZERG_FLYER_UPGRADE_PREFIX)
if used_item_names.isdisjoint(item_groups.protoss_ground_wa):
inventory = exclude_wa(item_names.PROTOSS_GROUND_UPGRADE_PREFIX)
if used_item_names.isdisjoint(item_groups.protoss_air_wa):
inventory = exclude_wa(item_names.PROTOSS_AIR_UPGRADE_PREFIX)
# Part 4: Last-ditch effort to reduce inventory size; upgrades can go in start inventory
current_inventory_size = len(inventory)
precollect_items = current_inventory_size - inventory_size - start_inventory_size - filler_amount
if precollect_items > 0:
promotable = [
item
for item in inventory
if ItemFilterFlags.StartInventory not in item.filter_flags
]
self.world.random.shuffle(promotable)
for item in promotable[:precollect_items]:
item.filter_flags |= ItemFilterFlags.StartInventory
start_inventory_size += 1
assert current_inventory_size - start_inventory_size <= inventory_size - filler_amount, (
f"Couldn't reduce inventory to fit. target={inventory_size}, poolsize={current_inventory_size}, "
f"start_inventory={starcraft_item}, filler_amount={filler_amount}"
)
return inventory
def filter_items(world: 'SC2World', location_cache: List[Location], item_pool: List[StarcraftItem]) -> List[StarcraftItem]:
"""
Returns a semi-randomly pruned set of items based on number of available locations.
The returned inventory must be capable of logically accessing every location in the world.
"""
open_locations = [location for location in location_cache if location.item is None]
inventory_size = len(open_locations)
# Most of the excluded locations get actually removed but Victory ones are mandatory in order to allow the game
# to progress normally. Since regular items aren't flagged as filler, we need to generate enough filler for those
# locations as we need to have something that can be actually placed there.
# Therefore, we reserve those to be filler.
excluded_locations = [location for location in open_locations if location.name in world.options.exclude_locations.value]
reserved_filler_count = len(excluded_locations)
target_nonfiller_item_count = inventory_size - reserved_filler_count
filler_amount = (inventory_size * world.options.filler_percentage) // 100
if world.options.required_tactics.value == RequiredTactics.option_no_logic:
mission_requirements = []
else:
mission_requirements = [(location.name, location.access_rule) for location in location_cache]
valid_inventory = ValidInventory(world, item_pool)
valid_items = valid_inventory.generate_reduced_inventory(target_nonfiller_item_count, filler_amount, mission_requirements)
for _ in range(reserved_filler_count):
filler_item = world.create_item(world.get_filler_item_name())
if filler_item.classification & ItemClassification.progression:
filler_item.classification = ItemClassification.filler # Must be flagged as Filler, even if it's a Kerrigan level
valid_items.append(filler_item)
return valid_items

532
worlds/sc2/regions.py Normal file
View File

@@ -0,0 +1,532 @@
from typing import TYPE_CHECKING, List, Dict, Any, Tuple, Optional
from Options import OptionError
from .locations import LocationData, Location
from .mission_tables import (
SC2Mission, SC2Campaign, MissionFlag, get_campaign_goal_priority,
campaign_final_mission_locations, campaign_alt_final_mission_locations
)
from .options import (
ShuffleNoBuild, RequiredTactics, ShuffleCampaigns,
kerrigan_unit_available, TakeOverAIAllies, MissionOrder, get_excluded_missions, get_enabled_campaigns,
static_mission_orders,
TwoStartPositions, KeyMode, EnableMissionRaceBalancing, EnableRaceSwapVariants, NovaGhostOfAChanceVariant,
WarCouncilNerfs, GrantStoryTech
)
from .mission_order.options import CustomMissionOrder
from .mission_order import SC2MissionOrder
from .mission_order.nodes import SC2MOGenMissionOrder, Difficulty
from .mission_order.mission_pools import SC2MOGenMissionPools
from .mission_order.generation import resolve_unlocks, fill_depths, resolve_difficulties, fill_missions, make_connections, resolve_generic_keys
if TYPE_CHECKING:
from . import SC2World
def create_mission_order(
world: 'SC2World', locations: Tuple[LocationData, ...], location_cache: List[Location]
):
# 'locations' contains both actual game locations and beat event locations for all mission regions
# When a region (mission) is accessible, all its locations are potentially accessible
# Accessible in this context always means "its access rule evaluates to True"
# This includes the beat events, which copy the access rules of the victory locations
# Beat events being added to logical inventory is auto-magic:
# Event locations contain an event item of (by default) identical name,
# which Archipelago's generator will consider part of the logical inventory
# whenever the event location becomes accessible
# Set up mission pools
mission_pools = SC2MOGenMissionPools()
mission_pools.set_exclusions(get_excluded_missions(world), []) # TODO set unexcluded
adjust_mission_pools(world, mission_pools)
setup_mission_pool_balancing(world, mission_pools)
mission_order_type = world.options.mission_order
if mission_order_type == MissionOrder.option_custom:
mission_order_dict = world.options.custom_mission_order.value
else:
mission_order_option = create_regular_mission_order(world, mission_pools)
if mission_order_type in static_mission_orders:
# Static orders get converted early to curate preset content, so it can be used as-is
mission_order_dict = mission_order_option
else:
mission_order_dict = CustomMissionOrder(mission_order_option).value
mission_order = SC2MOGenMissionOrder(world, mission_order_dict)
# Set up requirements for individual parts of the mission order
resolve_unlocks(mission_order)
# Ensure total accessibilty and resolve relative difficulties
fill_depths(mission_order)
resolve_difficulties(mission_order)
# Build the mission order
fill_missions(mission_order, mission_pools, world, [], locations, location_cache) # TODO set locked missions
make_connections(mission_order, world)
# Fill in Key requirements now that missions are placed
resolve_generic_keys(mission_order)
return SC2MissionOrder(mission_order, mission_pools)
def adjust_mission_pools(world: 'SC2World', pools: SC2MOGenMissionPools):
# Mission pool changes
mission_order_type = world.options.mission_order.value
enabled_campaigns = get_enabled_campaigns(world)
adv_tactics = world.options.required_tactics.value != RequiredTactics.option_standard
shuffle_no_build = world.options.shuffle_no_build.value
extra_locations = world.options.extra_locations.value
grant_story_tech = world.options.grant_story_tech.value
grant_story_levels = world.options.grant_story_levels.value
war_council_nerfs = world.options.war_council_nerfs.value == WarCouncilNerfs.option_true
# WoL
if shuffle_no_build == ShuffleNoBuild.option_false or adv_tactics:
# Replacing No Build missions with Easy missions
# WoL
pools.move_mission(SC2Mission.ZERO_HOUR, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.EVACUATION, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.EVACUATION_Z, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.EVACUATION_P, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.DEVILS_PLAYGROUND, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.DEVILS_PLAYGROUND_Z, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.DEVILS_PLAYGROUND_P, Difficulty.EASY, Difficulty.STARTER)
if world.options.required_tactics != RequiredTactics.option_any_units:
# Per playtester feedback: doing this mission with only one unit is flaky
# but there are enough viable comps that >= 2 random units is probably workable
pools.move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY_P, Difficulty.EASY, Difficulty.STARTER)
# LotV
pools.move_mission(SC2Mission.THE_GROWING_SHADOW, Difficulty.EASY, Difficulty.STARTER)
if shuffle_no_build == ShuffleNoBuild.option_false:
# Pushing Outbreak to Normal, as it cannot be placed as the second mission on Build-Only
pools.move_mission(SC2Mission.OUTBREAK, Difficulty.EASY, Difficulty.MEDIUM)
# Pushing extra Normal missions to Easy
pools.move_mission(SC2Mission.ECHOES_OF_THE_FUTURE, Difficulty.MEDIUM, Difficulty.EASY)
pools.move_mission(SC2Mission.CUTTHROAT, Difficulty.MEDIUM, Difficulty.EASY)
# Additional changes on Advanced Tactics
if adv_tactics:
# WoL
pools.move_mission(SC2Mission.SMASH_AND_GRAB, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.THE_MOEBIUS_FACTOR, Difficulty.MEDIUM, Difficulty.EASY)
pools.move_mission(SC2Mission.THE_MOEBIUS_FACTOR_Z, Difficulty.MEDIUM, Difficulty.EASY)
pools.move_mission(SC2Mission.THE_MOEBIUS_FACTOR_P, Difficulty.MEDIUM, Difficulty.EASY)
pools.move_mission(SC2Mission.WELCOME_TO_THE_JUNGLE, Difficulty.MEDIUM, Difficulty.EASY)
pools.move_mission(SC2Mission.ENGINE_OF_DESTRUCTION, Difficulty.HARD, Difficulty.MEDIUM)
# Prophecy needs to be adjusted if by itself
if enabled_campaigns == {SC2Campaign.PROPHECY}:
pools.move_mission(SC2Mission.A_SINISTER_TURN, Difficulty.MEDIUM, Difficulty.EASY)
# Prologue's only valid starter is the goal mission
if enabled_campaigns == {SC2Campaign.PROLOGUE} \
or mission_order_type in static_mission_orders \
and world.options.shuffle_campaigns.value == ShuffleCampaigns.option_false:
pools.move_mission(SC2Mission.DARK_WHISPERS, Difficulty.EASY, Difficulty.STARTER)
# HotS
kerriganless = world.options.kerrigan_presence.value not in kerrigan_unit_available \
or SC2Campaign.HOTS not in enabled_campaigns
if grant_story_tech == GrantStoryTech.option_grant:
# Additional starter mission if player is granted story tech
pools.move_mission(SC2Mission.ENEMY_WITHIN, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.TEMPLAR_S_RETURN, Difficulty.MEDIUM, Difficulty.STARTER)
pools.move_mission(SC2Mission.THE_ESCAPE, Difficulty.MEDIUM, Difficulty.STARTER)
pools.move_mission(SC2Mission.IN_THE_ENEMY_S_SHADOW, Difficulty.MEDIUM, Difficulty.STARTER)
if not war_council_nerfs:
pools.move_mission(SC2Mission.TEMPLAR_S_RETURN, Difficulty.MEDIUM, Difficulty.STARTER)
if (grant_story_tech == GrantStoryTech.option_grant and grant_story_levels) or kerriganless:
# The player has, all the stuff he needs, provided under these settings
pools.move_mission(SC2Mission.SUPREME, Difficulty.MEDIUM, Difficulty.STARTER)
pools.move_mission(SC2Mission.THE_INFINITE_CYCLE, Difficulty.HARD, Difficulty.STARTER)
pools.move_mission(SC2Mission.CONVICTION, Difficulty.MEDIUM, Difficulty.STARTER)
if (grant_story_tech != GrantStoryTech.option_grant
and (
world.options.nova_ghost_of_a_chance_variant == NovaGhostOfAChanceVariant.option_nco
or (
SC2Campaign.NCO in enabled_campaigns
and world.options.nova_ghost_of_a_chance_variant.value == NovaGhostOfAChanceVariant.option_auto
)
)
):
# Using NCO tech for this mission that must be acquired
pools.move_mission(SC2Mission.GHOST_OF_A_CHANCE, Difficulty.STARTER, Difficulty.MEDIUM)
if world.options.take_over_ai_allies.value == TakeOverAIAllies.option_true:
pools.move_mission(SC2Mission.HARBINGER_OF_OBLIVION, Difficulty.MEDIUM, Difficulty.STARTER)
if pools.get_pool_size(Difficulty.STARTER) < 2 and not kerriganless or adv_tactics:
# Conditionally moving Easy missions to Starter
pools.move_mission(SC2Mission.HARVEST_OF_SCREAMS, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.DOMINATION, Difficulty.EASY, Difficulty.STARTER)
if pools.get_pool_size(Difficulty.STARTER) < 2:
pools.move_mission(SC2Mission.DOMINATION, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.DOMINATION_T, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.DOMINATION_P, Difficulty.EASY, Difficulty.STARTER)
if pools.get_pool_size(Difficulty.STARTER) + pools.get_pool_size(Difficulty.EASY) < 2:
# Flashpoint needs just a few items at start but competent comp at the end
pools.move_mission(SC2Mission.FLASHPOINT, Difficulty.HARD, Difficulty.EASY)
def setup_mission_pool_balancing(world: 'SC2World', pools: SC2MOGenMissionPools):
race_mission_balance = world.options.mission_race_balancing.value
flag_ratios: Dict[MissionFlag, int] = {}
flag_weights: Dict[MissionFlag, int] = {}
if race_mission_balance == EnableMissionRaceBalancing.option_semi_balanced:
flag_weights = { MissionFlag.Terran: 1, MissionFlag.Zerg: 1, MissionFlag.Protoss: 1 }
elif race_mission_balance == EnableMissionRaceBalancing.option_fully_balanced:
flag_ratios = { MissionFlag.Terran: 1, MissionFlag.Zerg: 1, MissionFlag.Protoss: 1 }
pools.set_flag_balances(flag_ratios, flag_weights)
def create_regular_mission_order(world: 'SC2World', mission_pools: SC2MOGenMissionPools) -> Dict[str, Dict[str, Any]]:
mission_order_type = world.options.mission_order.value
if mission_order_type in static_mission_orders:
return create_static_mission_order(world, mission_order_type, mission_pools)
else:
return create_dynamic_mission_order(world, mission_order_type, mission_pools)
def create_static_mission_order(world: 'SC2World', mission_order_type: int, mission_pools: SC2MOGenMissionPools) -> Dict[str, Dict[str, Any]]:
mission_order: Dict[str, Dict[str, Any]] = {}
enabled_campaigns = get_enabled_campaigns(world)
if mission_order_type == MissionOrder.option_vanilla:
missions = "vanilla"
elif world.options.shuffle_campaigns.value == ShuffleCampaigns.option_true:
missions = "random"
else:
missions = "vanilla_shuffled"
if world.options.enable_race_swap.value == EnableRaceSwapVariants.option_disabled:
shuffle_raceswaps = False
else:
# Picking specific raceswap variants is handled by mission exclusion
shuffle_raceswaps = True
key_mode_option = world.options.key_mode.value
if key_mode_option == KeyMode.option_missions:
keys = "missions"
elif key_mode_option == KeyMode.option_questlines:
keys = "layouts"
elif key_mode_option == KeyMode.option_progressive_missions:
keys = "progressive_missions"
elif key_mode_option == KeyMode.option_progressive_questlines:
keys = "progressive_layouts"
elif key_mode_option == KeyMode.option_progressive_per_questline:
keys = "progressive_per_layout"
else:
keys = "none"
if mission_order_type == MissionOrder.option_mini_campaign:
prefix = "mini "
else:
prefix = ""
def mission_order_preset(name: str) -> Dict[str, str]:
return {
"preset": prefix + name,
"missions": missions,
"shuffle_raceswaps": shuffle_raceswaps,
"keys": keys
}
prophecy_enabled = SC2Campaign.PROPHECY in enabled_campaigns
wol_enabled = SC2Campaign.WOL in enabled_campaigns
if wol_enabled:
mission_order[SC2Campaign.WOL.campaign_name] = mission_order_preset("wol")
if prophecy_enabled:
mission_order[SC2Campaign.PROPHECY.campaign_name] = mission_order_preset("prophecy")
if SC2Campaign.HOTS in enabled_campaigns:
mission_order[SC2Campaign.HOTS.campaign_name] = mission_order_preset("hots")
if SC2Campaign.PROLOGUE in enabled_campaigns:
mission_order[SC2Campaign.PROLOGUE.campaign_name] = mission_order_preset("prologue")
if SC2Campaign.LOTV in enabled_campaigns:
mission_order[SC2Campaign.LOTV.campaign_name] = mission_order_preset("lotv")
if SC2Campaign.EPILOGUE in enabled_campaigns:
mission_order[SC2Campaign.EPILOGUE.campaign_name] = mission_order_preset("epilogue")
entry_rules = []
if SC2Campaign.WOL in enabled_campaigns:
entry_rules.append({ "scope": SC2Campaign.WOL.campaign_name })
if SC2Campaign.HOTS in enabled_campaigns:
entry_rules.append({ "scope": SC2Campaign.HOTS.campaign_name })
if SC2Campaign.LOTV in enabled_campaigns:
entry_rules.append({ "scope": SC2Campaign.LOTV.campaign_name })
mission_order[SC2Campaign.EPILOGUE.campaign_name]["entry_rules"] = entry_rules
if SC2Campaign.NCO in enabled_campaigns:
mission_order[SC2Campaign.NCO.campaign_name] = mission_order_preset("nco")
# Resolve immediately so the layout updates are simpler
mission_order = CustomMissionOrder(mission_order).value
# WoL requirements should count missions from Prophecy if both are enabled, and Prophecy should require a WoL mission
# There is a preset that already does this, but special-casing this way is easier to work with for other code
if wol_enabled and prophecy_enabled:
fix_wol_prophecy_entry_rules(mission_order)
# Vanilla Shuffled is allowed to drop some slots
if mission_order_type == MissionOrder.option_vanilla_shuffled:
remove_missions(world, mission_order, mission_pools)
# Curate final missions and goal campaigns
force_final_missions(world, mission_order, mission_order_type)
return mission_order
def fix_wol_prophecy_entry_rules(mission_order: Dict[str, Dict[str, Any]]):
prophecy_name = SC2Campaign.PROPHECY.campaign_name
# Make the mission count entry rules in WoL also count Prophecy
def fix_entry_rule(entry_rule: Dict[str, Any], local_campaign_scope: str):
# This appends Prophecy to any scope that points at the local campaign (WoL)
if "scope" in entry_rule:
if entry_rule["scope"] == local_campaign_scope:
entry_rule["scope"] = [local_campaign_scope, prophecy_name]
elif isinstance(entry_rule["scope"], list) and local_campaign_scope in entry_rule["scope"]:
entry_rule["scope"] = entry_rule["scope"] + [prophecy_name]
for layout_dict in mission_order[SC2Campaign.WOL.campaign_name].values():
if not isinstance(layout_dict, dict):
continue
if "entry_rules" in layout_dict:
for entry_rule in layout_dict["entry_rules"]:
fix_entry_rule(entry_rule, "..")
if "missions" in layout_dict:
for mission_dict in layout_dict["missions"]:
if "entry_rules" in mission_dict:
for entry_rule in mission_dict["entry_rules"]:
fix_entry_rule(entry_rule, "../..")
# Make Prophecy require Artifact's second mission
mission_order[prophecy_name][prophecy_name]["entry_rules"] = [{ "scope": [f"{SC2Campaign.WOL.campaign_name}/Artifact/1"]}]
def force_final_missions(world: 'SC2World', mission_order: Dict[str, Dict[str, Any]], mission_order_type: int):
goal_mission: Optional[SC2Mission] = None
excluded_missions = get_excluded_missions(world)
enabled_campaigns = get_enabled_campaigns(world)
raceswap_variants = [mission for mission in SC2Mission if mission.flags & MissionFlag.RaceSwap]
# Prefer long campaigns over shorter ones and harder missions over easier ones
goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns}
goal_level = max(goal_priorities.values())
candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
candidate_campaigns.sort(key=lambda it: it.id)
# Vanilla Shuffled & Mini Campaign get a curated final mission
if mission_order_type != MissionOrder.option_vanilla:
for goal_campaign in candidate_campaigns:
primary_goal = campaign_final_mission_locations[goal_campaign]
if primary_goal is None or primary_goal.mission in excluded_missions:
# No primary goal or its mission is excluded
candidate_missions = list(campaign_alt_final_mission_locations[goal_campaign].keys())
# Also allow raceswaps of curated final missions, provided they're not excluded
for candidate_with_raceswaps in [mission for mission in candidate_missions if mission.flags & MissionFlag.HasRaceSwap]:
raceswap_candidates = [mission for mission in raceswap_variants if mission.map_file == candidate_with_raceswaps.map_file]
candidate_missions.extend(raceswap_candidates)
candidate_missions = [mission for mission in candidate_missions if mission not in excluded_missions]
if len(candidate_missions) == 0:
raise OptionError(f"There are no valid goal missions for campaign {goal_campaign.campaign_name}. Please exclude fewer missions.")
goal_mission = world.random.choice(candidate_missions)
else:
goal_mission = primary_goal.mission
# The goal layout for static presets is the layout corresponding to the last key
goal_layout = list(mission_order[goal_campaign.campaign_name].keys())[-1]
goal_index = mission_order[goal_campaign.campaign_name][goal_layout]["size"] - 1
mission_order[goal_campaign.campaign_name][goal_layout]["missions"].append({
"index": [goal_index],
"mission_pool": [goal_mission.id]
})
# Remove goal status from lower priority campaigns
for campaign in enabled_campaigns:
if campaign not in candidate_campaigns:
mission_order[campaign.campaign_name]["goal"] = False
def remove_missions(world: 'SC2World', mission_order: Dict[str, Dict[str, Any]], mission_pools: SC2MOGenMissionPools):
enabled_campaigns = get_enabled_campaigns(world)
removed_counts: Dict[SC2Campaign, Dict[str, int]] = {}
for campaign in enabled_campaigns:
# Count missing missions for each campaign individually
campaign_size = sum(layout["size"] for layout in mission_order[campaign.campaign_name].values() if type(layout) == dict)
allowed_missions = mission_pools.count_allowed_missions(campaign)
removal_count = campaign_size - allowed_missions
if removal_count > len(removal_priorities[campaign]):
raise OptionError(f"Too many missions of campaign {campaign.campaign_name} excluded, cannot fill vanilla shuffled mission order.")
for layout in removal_priorities[campaign][:removal_count]:
removed_counts.setdefault(campaign, {}).setdefault(layout, 0)
removed_counts[campaign][layout] += 1
mission_order[campaign.campaign_name][layout]["size"] -= 1
# Fix mission indices & nexts
for (campaign, layouts) in removed_counts.items():
for (layout, amount) in layouts.items():
new_size = mission_order[campaign.campaign_name][layout]["size"]
original_size = new_size + amount
for removed_idx in range(new_size, original_size):
for mission in mission_order[campaign.campaign_name][layout]["missions"]:
if "index" in mission and removed_idx in mission["index"]:
mission["index"].remove(removed_idx)
if "next" in mission and removed_idx in mission["next"]:
mission["next"].remove(removed_idx)
# Special cases
if SC2Campaign.WOL in removed_counts:
if "Char" in removed_counts[SC2Campaign.WOL]:
# Remove the first two mission changes that create the branching path
mission_order[SC2Campaign.WOL.campaign_name]["Char"]["missions"] = mission_order[SC2Campaign.WOL.campaign_name]["Char"]["missions"][2:]
if SC2Campaign.NCO in removed_counts:
# Remove the whole last layout if its size is 0
if "Mission Pack 3" in removed_counts[SC2Campaign.NCO] and removed_counts[SC2Campaign.NCO]["Mission Pack 3"] == 3:
mission_order[SC2Campaign.NCO.campaign_name].pop("Mission Pack 3")
removal_priorities: Dict[SC2Campaign, List[str]] = {
SC2Campaign.WOL: [
"Colonist",
"Covert",
"Covert",
"Char",
"Rebellion",
"Artifact",
"Artifact",
"Rebellion"
],
SC2Campaign.PROPHECY: [
"Prophecy",
"Prophecy"
],
SC2Campaign.HOTS: [
"Umoja",
"Kaldir",
"Char",
"Zerus",
"Skygeirr Station"
],
SC2Campaign.PROLOGUE: [
"Prologue",
],
SC2Campaign.LOTV: [
"Ulnar",
"Return to Aiur",
"Aiur",
"Tal'darim",
"Purifier",
"Shakuras",
"Korhal"
],
SC2Campaign.EPILOGUE: [
"Epilogue",
],
SC2Campaign.NCO: [
"Mission Pack 3",
"Mission Pack 3",
"Mission Pack 2",
"Mission Pack 2",
"Mission Pack 1",
"Mission Pack 1",
"Mission Pack 3"
]
}
def make_grid(world: 'SC2World', size: int) -> Dict[str, Dict[str, Any]]:
mission_order = {
"grid": {
"display_name": "",
"type": "grid",
"size": size,
"two_start_positions": world.options.two_start_positions.value == TwoStartPositions.option_true
}
}
return mission_order
def make_golden_path(world: 'SC2World', size: int) -> Dict[str, Dict[str, Any]]:
key_mode = world.options.key_mode.value
if key_mode == KeyMode.option_missions:
keys = "missions"
elif key_mode == KeyMode.option_questlines:
keys = "layouts"
elif key_mode == KeyMode.option_progressive_missions:
keys = "progressive_missions"
elif key_mode == KeyMode.option_progressive_questlines:
keys = "progressive_layouts"
elif key_mode == KeyMode.option_progressive_per_questline:
keys = "progressive_per_layout"
else:
keys = "none"
mission_order = {
"golden path": {
"display_name": "",
"preset": "golden path",
"size": size,
"keys": keys,
"two_start_positions": world.options.two_start_positions.value == TwoStartPositions.option_true
}
}
return mission_order
def make_gauntlet(size: int) -> Dict[str, Dict[str, Any]]:
mission_order = {
"gauntlet": {
"display_name": "",
"type": "gauntlet",
"size": size,
}
}
return mission_order
def make_blitz(size: int) -> Dict[str, Dict[str, Any]]:
mission_order = {
"blitz": {
"display_name": "",
"type": "blitz",
"size": size,
}
}
return mission_order
def make_hopscotch(world: 'SC2World', size: int) -> Dict[str, Dict[str, Any]]:
mission_order = {
"hopscotch": {
"display_name": "",
"type": "hopscotch",
"size": size,
"two_start_positions": world.options.two_start_positions.value == TwoStartPositions.option_true
}
}
return mission_order
def create_dynamic_mission_order(world: 'SC2World', mission_order_type: int, mission_pools: SC2MOGenMissionPools) -> Dict[str, Dict[str, Any]]:
num_missions = min(mission_pools.get_allowed_mission_count(), world.options.maximum_campaign_size.value)
num_missions = max(1, num_missions)
if mission_order_type == MissionOrder.option_golden_path:
return make_golden_path(world, num_missions)
if mission_order_type == MissionOrder.option_grid:
mission_order = make_grid(world, num_missions)
elif mission_order_type == MissionOrder.option_gauntlet:
mission_order = make_gauntlet(num_missions)
elif mission_order_type == MissionOrder.option_blitz:
mission_order = make_blitz(num_missions)
elif mission_order_type == MissionOrder.option_hopscotch:
mission_order = make_hopscotch(world, num_missions)
else:
raise ValueError("Received unknown Mission Order type")
# Optionally add key requirements
# This only works for layout types that don't define their own entry rules (which is currently all of them)
# Golden Path handles Key Mode on its own
key_mode = world.options.key_mode.value
if key_mode == KeyMode.option_missions:
mission_order[list(mission_order.keys())[0]]["missions"] = [
{ "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] },
{ "index": "entrances", "entry_rules": [] }
]
elif key_mode == KeyMode.option_progressive_missions:
mission_order[list(mission_order.keys())[0]]["missions"] = [
{ "index": "all", "entry_rules": [{ "items": { "Progressive Key": 1 }}] },
{ "index": "entrances", "entry_rules": [] }
]
return mission_order

3582
worlds/sc2/rules.py Normal file

File diff suppressed because it is too large Load Diff

49
worlds/sc2/settings.py Normal file
View File

@@ -0,0 +1,49 @@
from typing import Union
import settings
class Starcraft2Settings(settings.Group):
class WindowWidth(int):
"""The starting width the client window in pixels"""
class WindowHeight(int):
"""The starting height the client window in pixels"""
class GameWindowedMode(settings.Bool):
"""Controls whether the game should start in windowed mode"""
class TerranButtonColor(list):
"""Defines the colour of terran mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)"""
class ZergButtonColor(list):
"""Defines the colour of zerg mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)"""
class ProtossButtonColor(list):
"""Defines the colour of protoss mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)"""
class DisableForcedCamera(str):
"""Overrides the disable forced-camera slot option. Possible values: `true`, `false`, `default`. Default uses slot value"""
class SkipCutscenes(str):
"""Overrides the skip cutscenes slot option. Possible values: `true`, `false`, `default`. Default uses slot value"""
class GameDifficulty(str):
"""Overrides the slot's difficulty setting. Possible values: `casual`, `normal`, `hard`, `brutal`, `default`. Default uses slot value"""
class GameSpeed(str):
"""Overrides the slot's gamespeed setting. Possible values: `slower`, `slow`, `normal`, `fast`, `faster`, `default`. Default uses slot value"""
class ShowTraps(settings.Bool):
"""If set to true, in-client scouting will show traps as distinct from filler"""
window_width: WindowWidth = WindowWidth(1080)
window_height: WindowHeight = WindowHeight(720)
game_windowed_mode: Union[GameWindowedMode, bool] = False
show_traps: Union[ShowTraps, bool] = False
disable_forced_camera: DisableForcedCamera = DisableForcedCamera("default")
skip_cutscenes: SkipCutscenes = SkipCutscenes("default")
game_difficulty: GameDifficulty = GameDifficulty("default")
game_speed: GameSpeed = GameSpeed("default")
terran_button_color: TerranButtonColor = TerranButtonColor([0.0838, 0.2898, 0.2346])
zerg_button_color: ZergButtonColor = ZergButtonColor([0.345, 0.22425, 0.12765])
protoss_button_color: ProtossButtonColor = ProtossButtonColor([0.18975, 0.2415, 0.345])

61
worlds/sc2/starcraft2.kv Normal file
View File

@@ -0,0 +1,61 @@
<CampaignScroll>
scroll_type: ["content", "bars"]
bar_width: dp(12)
effect_cls: "ScrollEffect"
canvas.after:
Color:
rgba: (0.82, 0.2, 0, root.border_on)
Line:
width: 1.5
rectangle: self.x+1, self.y+1, self.width-1, self.height-1
<DownloadDataWarningMessage>
color: (1, 1, 1, 1)
canvas.before:
Color:
rgba: (0xd2/0xff, 0x33/0xff, 0, 1)
Rectangle:
pos: (self.x - 8, self.y - 8)
size: (self.width + 30, self.height + 16)
<MultiCampaignLayout>
cols: 1
size_hint_y: None
height: self.minimum_height + 15
padding: [5,0,dp(12),0]
<CampaignLayout>:
cols: 1
<MissionLayout>:
rows: 1
<RegionLayout>:
cols: 1
<ColumnLayout>:
rows: 1
<MissionCategory>:
cols: 1
spacing: [0,5]
<MissionButton>:
text_size: self.size
markup: True
halign: 'center'
valign: 'middle'
padding: [5,0,5,0]
outline_width: 1
canvas.before:
Color:
rgba: (1, 193/255, 86/255, root.is_goal)
Line:
width: 1
rectangle: (self.x, self.y + 0.5, self.width, self.height)
canvas.after:
Color:
rgba: (0.8, 0.8, 0.8, root.is_exit)
Line:
width: 1
rectangle: (self.x + 2, self.y + 3, self.width - 4, self.height - 4)

View File

@@ -1,41 +0,0 @@
import unittest
from .test_base import Sc2TestBase
from .. import Regions
from .. import Options, MissionTables
class TestGridsizes(unittest.TestCase):
def test_grid_sizes_meet_specs(self):
self.assertTupleEqual((1, 2, 0), Regions.get_grid_dimensions(2))
self.assertTupleEqual((1, 3, 0), Regions.get_grid_dimensions(3))
self.assertTupleEqual((2, 2, 0), Regions.get_grid_dimensions(4))
self.assertTupleEqual((2, 3, 1), Regions.get_grid_dimensions(5))
self.assertTupleEqual((2, 4, 1), Regions.get_grid_dimensions(7))
self.assertTupleEqual((2, 4, 0), Regions.get_grid_dimensions(8))
self.assertTupleEqual((3, 3, 0), Regions.get_grid_dimensions(9))
self.assertTupleEqual((2, 5, 0), Regions.get_grid_dimensions(10))
self.assertTupleEqual((3, 4, 1), Regions.get_grid_dimensions(11))
self.assertTupleEqual((3, 4, 0), Regions.get_grid_dimensions(12))
self.assertTupleEqual((3, 5, 0), Regions.get_grid_dimensions(15))
self.assertTupleEqual((4, 4, 0), Regions.get_grid_dimensions(16))
self.assertTupleEqual((4, 6, 0), Regions.get_grid_dimensions(24))
self.assertTupleEqual((5, 5, 0), Regions.get_grid_dimensions(25))
self.assertTupleEqual((5, 6, 1), Regions.get_grid_dimensions(29))
self.assertTupleEqual((5, 7, 2), Regions.get_grid_dimensions(33))
class TestGridGeneration(Sc2TestBase):
options = {
"mission_order": Options.MissionOrder.option_grid,
"excluded_missions": [MissionTables.SC2Mission.ZERO_HOUR.mission_name,],
"enable_hots_missions": False,
"enable_prophecy_missions": True,
"enable_lotv_prologue_missions": False,
"enable_lotv_missions": False,
"enable_epilogue_missions": False,
"enable_nco_missions": False
}
def test_size_matches_exclusions(self):
self.assertNotIn(MissionTables.SC2Mission.ZERO_HOUR.mission_name, self.multiworld.regions)
# WoL has 29 missions. -1 for Zero Hour being excluded, +1 for the automatically-added menu location
self.assertEqual(len(self.multiworld.regions), 29)

View File

@@ -1,11 +1,52 @@
from typing import *
import unittest
import random
from argparse import Namespace
from BaseClasses import MultiWorld, CollectionState, PlandoOptions
from Generate import get_seed_name
from worlds import AutoWorld
from test.general import gen_steps, call_all
from test.TestBase import WorldTestBase
from test.bases import WorldTestBase
from .. import SC2World
from .. import Client
from .. import client
class Sc2TestBase(WorldTestBase):
game = Client.SC2Context.game
game = client.SC2Context.game
world: SC2World
player: ClassVar[int] = 1
skip_long_tests: bool = True
class Sc2SetupTestBase(unittest.TestCase):
"""
A custom sc2-specific test base class that provides an explicit function to generate the world from options.
This allows potentially generating multiple worlds in one test case, useful for tracking down a rare / sporadic
crash.
"""
seed: Optional[int] = None
game = SC2World.game
player = 1
def generate_world(self, options: Dict[str, Any]) -> None:
self.multiworld = MultiWorld(1)
self.multiworld.game[self.player] = self.game
self.multiworld.player_name = {self.player: "Tester"}
self.multiworld.set_seed(self.seed)
random.seed(self.multiworld.seed)
self.multiworld.seed_name = get_seed_name(random) # only called to get same RNG progression as Generate.py
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
new_option = option.from_any(options.get(name, option.default))
new_option.verify(SC2World, "Tester", PlandoOptions.items|PlandoOptions.connections|PlandoOptions.texts|PlandoOptions.bosses)
setattr(args, name, {
1: new_option
})
self.multiworld.set_options(args)
self.world: SC2World = cast(SC2World, self.multiworld.worlds[self.player])
self.multiworld.state = CollectionState(self.multiworld)
try:
for step in gen_steps:
call_all(self.multiworld, step)
except Exception as ex:
ex.add_note(f"Seed: {self.multiworld.seed}")
raise

View File

@@ -0,0 +1,216 @@
"""
Unit tests for custom mission orders
"""
from .test_base import Sc2SetupTestBase
from .. import MissionFlag
from ..item import item_tables, item_names
from BaseClasses import ItemClassification
class TestCustomMissionOrders(Sc2SetupTestBase):
def test_mini_wol_generates(self):
world_options = {
'mission_order': 'custom',
'custom_mission_order': {
'Mini Wings of Liberty': {
'global': {
'type': 'column',
'mission_pool': [
'terran missions',
'^ wol missions'
]
},
'Mar Sara': {
'size': 1
},
'Colonist': {
'size': 2,
'entry_rules': [{
'scope': '../Mar Sara'
}]
},
'Artifact': {
'size': 3,
'entry_rules': [{
'scope': '../Mar Sara'
}],
'missions': [
{
'index': 1,
'entry_rules': [{
'scope': 'Mini Wings of Liberty',
'amount': 4
}]
},
{
'index': 2,
'entry_rules': [{
'scope': 'Mini Wings of Liberty',
'amount': 8
}]
}
]
},
'Prophecy': {
'size': 2,
'entry_rules': [{
'scope': '../Artifact/1'
}],
'mission_pool': [
'protoss missions',
'^ prophecy missions'
]
},
'Covert': {
'size': 2,
'entry_rules': [{
'scope': 'Mini Wings of Liberty',
'amount': 2
}]
},
'Rebellion': {
'size': 2,
'entry_rules': [{
'scope': 'Mini Wings of Liberty',
'amount': 3
}]
},
'Char': {
'size': 3,
'entry_rules': [{
'scope': '../Artifact/2'
}],
'missions': [
{
'index': 0,
'next': [2]
},
{
'index': 1,
'entrance': True
}
]
}
}
}
}
self.generate_world(world_options)
flags = self.world.custom_mission_order.get_used_flags()
self.assertEqual(flags[MissionFlag.Terran], 13)
self.assertEqual(flags[MissionFlag.Protoss], 2)
self.assertEqual(flags.get(MissionFlag.Zerg, 0), 0)
sc2_regions = set(self.multiworld.regions.region_cache[self.player]) - {"Menu"}
self.assertEqual(len(self.world.custom_mission_order.get_used_missions()), len(sc2_regions))
def test_locked_and_necessary_item_appears_once(self):
# This is a filler upgrade with a parent
test_item = item_names.MARINE_OPTIMIZED_LOGISTICS
world_options = {
'mission_order': 'custom',
'locked_items': { test_item: 1 },
'custom_mission_order': {
'test': {
'type': 'column',
'size': 5, # Give the generator some space to place the key
'max_difficulty': 'easy',
'missions': [{
'index': 4,
'entry_rules': [{
'items': { test_item: 1 }
}]
}]
}
}
}
self.assertNotEqual(item_tables.item_table[test_item].classification, ItemClassification.progression, f"Test item {test_item} won't change classification")
self.generate_world(world_options)
test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item]
test_items_in_pool += [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item]
self.assertEqual(len(test_items_in_pool), 1)
self.assertEqual(test_items_in_pool[0].classification, ItemClassification.progression)
def test_start_inventory_and_necessary_item_appears_once(self):
# This is a filler upgrade with a parent
test_item = item_names.ZERGLING_METABOLIC_BOOST
world_options = {
'mission_order': 'custom',
'start_inventory': { test_item: 1 },
'custom_mission_order': {
'test': {
'type': 'column',
'size': 5, # Give the generator some space to place the key
'max_difficulty': 'easy',
'missions': [{
'index': 4,
'entry_rules': [{
'items': { test_item: 1 }
}]
}]
}
}
}
self.generate_world(world_options)
test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item]
self.assertEqual(len(test_items_in_pool), 0)
test_items_in_start_inventory = [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item]
self.assertEqual(len(test_items_in_start_inventory), 1)
def test_start_inventory_and_locked_and_necessary_item_appears_once(self):
# This is a filler upgrade with a parent
test_item = item_names.ZERGLING_METABOLIC_BOOST
world_options = {
'mission_order': 'custom',
'start_inventory': { test_item: 1 },
'locked_items': { test_item: 1 },
'custom_mission_order': {
'test': {
'type': 'column',
'size': 5, # Give the generator some space to place the key
'max_difficulty': 'easy',
'missions': [{
'index': 4,
'entry_rules': [{
'items': { test_item: 1 }
}]
}]
}
}
}
self.generate_world(world_options)
test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item]
self.assertEqual(len(test_items_in_pool), 0)
test_items_in_start_inventory = [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item]
self.assertEqual(len(test_items_in_start_inventory), 1)
def test_key_item_rule_creates_correct_item_amount(self):
# This is an item that normally only exists once
test_item = item_names.ZERGLING
test_amount = 3
world_options = {
'mission_order': 'custom',
'locked_items': { test_item: 1 }, # Make sure it is generated as normal
'custom_mission_order': {
'test': {
'type': 'column',
'size': 12, # Give the generator some space to place the keys
'max_difficulty': 'easy',
'mission_pool': ['zerg missions'], # Make sure the item isn't excluded by race selection
'missions': [{
'index': 10,
'entry_rules': [{
'items': { test_item: test_amount } # Require more than the usual item amount
}]
}]
}
}
}
self.generate_world(world_options)
test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item]
test_items_in_start_inventory = [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item]
self.assertEqual(len(test_items_in_pool + test_items_in_start_inventory), test_amount)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
"""
Unit tests for item filtering like pool_filter.py
"""
from .test_base import Sc2SetupTestBase
from ..item import item_groups, item_names
from .. import options
from ..mission_tables import SC2Race
class ItemFilterTests(Sc2SetupTestBase):
def test_excluding_all_barracks_units_excludes_infantry_upgrades(self) -> None:
world_options = {
'excluded_items': {
item_groups.ItemGroupNames.BARRACKS_UNITS: 0
},
'required_tactics': 'standard',
'min_number_of_upgrades': 1,
'selected_races': {
SC2Race.TERRAN.get_title()
},
'mission_order': 'grid',
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
races = {mission.race for mission in self.world.custom_mission_order.get_used_missions()}
self.assertIn(SC2Race.TERRAN, races)
self.assertNotIn(SC2Race.ZERG, races)
self.assertNotIn(SC2Race.PROTOSS, races)
itempool = [item.name for item in self.multiworld.itempool]
self.assertNotIn(item_names.MARINE, itempool)
self.assertNotIn(item_names.MARAUDER, itempool)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, itempool)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, itempool)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, itempool)
def test_excluding_one_item_of_multi_parent_doesnt_filter_children(self) -> None:
world_options = {
'locked_items': {
item_names.SENTINEL: 1,
item_names.CENTURION: 1,
},
'excluded_items': {
item_names.ZEALOT: 1,
# Exclude more items to make space
item_names.WRATHWALKER: 1,
item_names.ENERGIZER: 1,
item_names.AVENGER: 1,
item_names.ARBITER: 1,
item_names.VOID_RAY: 1,
item_names.PULSAR: 1,
item_names.DESTROYER: 1,
item_names.DAWNBRINGER: 1,
},
'min_number_of_upgrades': 2,
'required_tactics': 'standard',
'selected_races': {
SC2Race.PROTOSS.get_title()
},
'mission_order': 'grid',
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
itempool = [item.name for item in self.multiworld.itempool]
self.assertIn(item_names.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY, itempool)
self.assertIn(item_names.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS, itempool)
def test_excluding_all_items_in_multiparent_excludes_child_items(self) -> None:
world_options = {
'excluded_items': {
item_names.ZEALOT: 1,
item_names.SENTINEL: 1,
item_names.CENTURION: 1,
},
'min_number_of_upgrades': 2,
'required_tactics': 'standard',
'selected_races': {
SC2Race.PROTOSS.get_title()
},
'mission_order': 'grid',
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
itempool = [item.name for item in self.multiworld.itempool]
self.assertNotIn(item_names.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY, itempool)
self.assertNotIn(item_names.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS, itempool)

View File

@@ -0,0 +1,18 @@
import unittest
from ..item import item_descriptions, item_tables
class TestItemDescriptions(unittest.TestCase):
def test_all_items_have_description(self) -> None:
for item_name in item_tables.item_table:
self.assertIn(item_name, item_descriptions.item_descriptions)
def test_all_descriptions_refer_to_item_and_end_in_dot(self) -> None:
for item_name, item_desc in item_descriptions.item_descriptions.items():
self.assertIn(item_name, item_tables.item_table)
self.assertEqual(item_desc.strip()[-1], '.', msg=f"{item_name}'s item description does not end in a '.': '{item_desc}'")
def test_item_descriptions_follow_single_space_after_period_style(self) -> None:
for item_name, item_desc in item_descriptions.item_descriptions.items():
self.assertNotIn('. ', item_desc, f"Double-space after period in description for {item_name}")

View File

@@ -0,0 +1,32 @@
"""
Unit tests for item_groups.py
"""
import unittest
from ..item import item_groups, item_tables
class ItemGroupsUnitTests(unittest.TestCase):
def test_all_production_structure_groups_capture_all_units(self) -> None:
self.assertCountEqual(
item_groups.terran_units,
item_groups.barracks_units + item_groups.factory_units + item_groups.starport_units + item_groups.terran_mercenaries
)
self.assertCountEqual(
item_groups.protoss_units,
item_groups.gateway_units + item_groups.robo_units + item_groups.stargate_units
)
def test_terran_original_progressive_group_fully_contained_in_wol_upgrades(self) -> None:
for item_name in item_groups.terran_original_progressive_upgrades:
self.assertIn(item_tables.item_table[item_name].type, (
item_tables.TerranItemType.Progressive, item_tables.TerranItemType.Progressive_2), f"{item_name} is not progressive")
self.assertIn(item_name, item_groups.wol_upgrades)
def test_all_items_in_stimpack_group_are_stimpacks(self) -> None:
for item_name in item_groups.terran_stimpacks:
self.assertIn("Stimpack", item_name)
def test_all_item_group_names_have_a_group_defined(self) -> None:
for display_name in item_groups.ItemGroupNames.get_all_group_names():
self.assertIn(display_name, item_groups.item_name_groups)

View File

@@ -0,0 +1,170 @@
import unittest
from typing import List, Set
from ..item import item_tables
class TestItems(unittest.TestCase):
def test_grouped_upgrades_number(self) -> None:
"""
Tests if grouped upgrades have set number correctly
"""
bundled_items = item_tables.upgrade_bundles.keys()
bundled_item_data = [item_tables.get_full_item_list()[item_name] for item_name in bundled_items]
bundled_item_numbers = [item_data.number for item_data in bundled_item_data]
check_numbers = [number == -1 for number in bundled_item_numbers]
self.assertNotIn(False, check_numbers)
def test_non_grouped_upgrades_number(self) -> None:
"""
Checks if non-grouped upgrades number is set correctly thus can be sent into the game.
"""
check_modulo = 4
bundled_items = item_tables.upgrade_bundles.keys()
non_bundled_upgrades = [
item_name for item_name in item_tables.get_full_item_list().keys()
if (item_name not in bundled_items
and item_tables.get_full_item_list()[item_name].type in item_tables.upgrade_item_types)
]
non_bundled_upgrade_data = [item_tables.get_full_item_list()[item_name] for item_name in non_bundled_upgrades]
non_bundled_upgrade_numbers = [item_data.number for item_data in non_bundled_upgrade_data]
check_numbers = [number % check_modulo == 0 for number in non_bundled_upgrade_numbers]
self.assertNotIn(False, check_numbers)
def test_bundles_contain_only_basic_elements(self) -> None:
"""
Checks if there are no bundles within bundles.
"""
bundled_items = item_tables.upgrade_bundles.keys()
bundle_elements: List[str] = [item_name for values in item_tables.upgrade_bundles.values() for item_name in values]
for element in bundle_elements:
self.assertNotIn(element, bundled_items)
def test_weapon_armor_level(self) -> None:
"""
Checks if Weapon/Armor upgrade level is correctly set to all Weapon/Armor upgrade items.
"""
weapon_armor_upgrades = [item for item in item_tables.get_full_item_list() if item_tables.get_item_table()[item].type in item_tables.upgrade_item_types]
for weapon_armor_upgrade in weapon_armor_upgrades:
self.assertEqual(item_tables.get_full_item_list()[weapon_armor_upgrade].quantity, item_tables.WEAPON_ARMOR_UPGRADE_MAX_LEVEL)
def test_item_ids_distinct(self) -> None:
"""
Verifies if there are no duplicates of item ID.
"""
item_ids: Set[int] = {item_tables.get_full_item_list()[item_name].code for item_name in item_tables.get_full_item_list()}
self.assertEqual(len(item_ids), len(item_tables.get_full_item_list()))
def test_number_distinct_in_item_type(self) -> None:
"""
Tests if each item is distinct for sending into the mod.
"""
item_types: List[item_tables.ItemTypeEnum] = [
*[item.value for item in item_tables.TerranItemType],
*[item.value for item in item_tables.ZergItemType],
*[item.value for item in item_tables.ProtossItemType],
*[item.value for item in item_tables.FactionlessItemType]
]
self.assertGreater(len(item_types), 0)
for item_type in item_types:
item_names: List[str] = [
item_name for item_name in item_tables.get_full_item_list()
if item_tables.get_full_item_list()[item_name].number >= 0 # Negative numbers have special meaning
and item_tables.get_full_item_list()[item_name].type == item_type
]
item_numbers: Set[int] = {item_tables.get_full_item_list()[item_name] for item_name in item_names}
self.assertEqual(len(item_names), len(item_numbers))
def test_progressive_has_quantity(self) -> None:
"""
:return:
"""
progressive_groups: List[item_tables.ItemTypeEnum] = [
item_tables.TerranItemType.Progressive,
item_tables.TerranItemType.Progressive_2,
item_tables.ProtossItemType.Progressive,
item_tables.ZergItemType.Progressive
]
quantities: List[int] = [
item_tables.get_full_item_list()[item].quantity for item in item_tables.get_full_item_list()
if item_tables.get_full_item_list()[item].type in progressive_groups
]
self.assertNotIn(1, quantities)
def test_non_progressive_quantity(self) -> None:
"""
Check if non-progressive items have quantity at most 1.
"""
non_progressive_single_entity_groups: List[item_tables.ItemTypeEnum] = [
# Terran
item_tables.TerranItemType.Unit,
item_tables.TerranItemType.Unit_2,
item_tables.TerranItemType.Mercenary,
item_tables.TerranItemType.Armory_1,
item_tables.TerranItemType.Armory_2,
item_tables.TerranItemType.Armory_3,
item_tables.TerranItemType.Armory_4,
item_tables.TerranItemType.Armory_5,
item_tables.TerranItemType.Armory_6,
item_tables.TerranItemType.Armory_7,
item_tables.TerranItemType.Building,
item_tables.TerranItemType.Laboratory,
item_tables.TerranItemType.Nova_Gear,
# Zerg
item_tables.ZergItemType.Unit,
item_tables.ZergItemType.Mercenary,
item_tables.ZergItemType.Morph,
item_tables.ZergItemType.Strain,
item_tables.ZergItemType.Mutation_1,
item_tables.ZergItemType.Mutation_2,
item_tables.ZergItemType.Mutation_3,
item_tables.ZergItemType.Evolution_Pit,
item_tables.ZergItemType.Ability,
# Protoss
item_tables.ProtossItemType.Unit,
item_tables.ProtossItemType.Unit_2,
item_tables.ProtossItemType.Building,
item_tables.ProtossItemType.Forge_1,
item_tables.ProtossItemType.Forge_2,
item_tables.ProtossItemType.Forge_3,
item_tables.ProtossItemType.Forge_4,
item_tables.ProtossItemType.Solarite_Core,
item_tables.ProtossItemType.Spear_Of_Adun
]
quantities: List[int] = [
item_tables.get_full_item_list()[item].quantity for item in item_tables.get_full_item_list()
if item_tables.get_full_item_list()[item].type in non_progressive_single_entity_groups
]
for quantity in quantities:
self.assertLessEqual(quantity, 1)
def test_item_number_less_than_30(self) -> None:
"""
Checks if all item numbers are within bounds supported by game mod.
"""
not_checked_item_types: List[item_tables.ItemTypeEnum] = [
item_tables.ZergItemType.Level
]
items_to_check: List[str] = [
item for item in item_tables.get_full_item_list()
if item_tables.get_full_item_list()[item].type not in not_checked_item_types
]
for item in items_to_check:
item_number = item_tables.get_full_item_list()[item].number
self.assertLess(item_number, 30)

View File

@@ -0,0 +1,37 @@
import unittest
from .. import location_groups
from ..mission_tables import SC2Mission, MissionFlag
class TestLocationGroups(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.location_groups = location_groups.get_location_groups()
def test_location_categories_have_a_group(self) -> None:
self.assertIn('Victory', self.location_groups)
self.assertIn(f'{SC2Mission.LIBERATION_DAY.mission_name}: Victory', self.location_groups['Victory'])
self.assertIn(f'{SC2Mission.IN_UTTER_DARKNESS.mission_name}: Defeat', self.location_groups['Victory'])
self.assertIn('Vanilla', self.location_groups)
self.assertIn(f'{SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name}: Close Relic', self.location_groups['Vanilla'])
self.assertIn('Extra', self.location_groups)
self.assertIn(f'{SC2Mission.SMASH_AND_GRAB.mission_name}: First Forcefield Area Busted', self.location_groups['Extra'])
self.assertIn('Challenge', self.location_groups)
self.assertIn(f'{SC2Mission.ZERO_HOUR.mission_name}: First Hatchery', self.location_groups['Challenge'])
self.assertIn('Mastery', self.location_groups)
self.assertIn(f'{SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name}: Protoss Cleared', self.location_groups['Mastery'])
def test_missions_have_a_group(self) -> None:
self.assertIn(SC2Mission.LIBERATION_DAY.mission_name, self.location_groups)
self.assertIn(f'{SC2Mission.LIBERATION_DAY.mission_name}: Victory', self.location_groups[SC2Mission.LIBERATION_DAY.mission_name])
self.assertIn(f'{SC2Mission.LIBERATION_DAY.mission_name}: Special Delivery', self.location_groups[SC2Mission.LIBERATION_DAY.mission_name])
def test_race_swapped_locations_share_a_group(self) -> None:
self.assertIn(MissionFlag.HasRaceSwap, SC2Mission.ZERO_HOUR.flags)
ZERO_HOUR = 'Zero Hour'
self.assertNotEqual(ZERO_HOUR, SC2Mission.ZERO_HOUR.mission_name)
self.assertIn(ZERO_HOUR, self.location_groups)
self.assertIn(f'{ZERO_HOUR}: Victory', self.location_groups)
self.assertIn(f'{SC2Mission.ZERO_HOUR.mission_name}: Victory', self.location_groups[f'{ZERO_HOUR}: Victory'])
self.assertIn(f'{SC2Mission.ZERO_HOUR_P.mission_name}: Victory', self.location_groups[f'{ZERO_HOUR}: Victory'])
self.assertIn(f'{SC2Mission.ZERO_HOUR_Z.mission_name}: Victory', self.location_groups[f'{ZERO_HOUR}: Victory'])

View File

@@ -0,0 +1,9 @@
import unittest
from .. import mission_groups
class TestMissionGroups(unittest.TestCase):
def test_all_mission_groups_are_defined_and_nonempty(self) -> None:
for mission_group_name in mission_groups.MissionGroupNames.get_all_group_names():
self.assertIn(mission_group_name, mission_groups.mission_groups)
self.assertTrue(mission_groups.mission_groups[mission_group_name])

View File

@@ -1,7 +1,19 @@
import unittest
from .test_base import Sc2TestBase
from .. import Options, MissionTables
from typing import Dict
from .. import options
from ..item import item_parents
class TestOptions(unittest.TestCase):
def test_campaign_size_option_max_matches_number_of_missions(self):
self.assertEqual(Options.MaximumCampaignSize.range_end, len(MissionTables.SC2Mission))
def test_unit_max_upgrades_matching_items(self) -> None:
upgrade_group_to_count: Dict[str, int] = {}
for parent_id, child_list in item_parents.parent_id_to_children.items():
main_parent = item_parents.parent_present[parent_id].constraint_group
if main_parent is None:
continue
upgrade_group_to_count.setdefault(main_parent, 0)
upgrade_group_to_count[main_parent] += len(child_list)
self.assertEqual(options.MAX_UPGRADES_OPTION, max(upgrade_group_to_count.values()))

View File

@@ -0,0 +1,40 @@
import unittest
from .test_base import Sc2TestBase
from .. import mission_tables, SC2Campaign
from .. import options
from ..mission_order.layout_types import Grid
class TestGridsizes(unittest.TestCase):
def test_grid_sizes_meet_specs(self):
self.assertTupleEqual((1, 2, 0), Grid.get_grid_dimensions(2))
self.assertTupleEqual((1, 3, 0), Grid.get_grid_dimensions(3))
self.assertTupleEqual((2, 2, 0), Grid.get_grid_dimensions(4))
self.assertTupleEqual((2, 3, 1), Grid.get_grid_dimensions(5))
self.assertTupleEqual((2, 4, 1), Grid.get_grid_dimensions(7))
self.assertTupleEqual((2, 4, 0), Grid.get_grid_dimensions(8))
self.assertTupleEqual((3, 3, 0), Grid.get_grid_dimensions(9))
self.assertTupleEqual((2, 5, 0), Grid.get_grid_dimensions(10))
self.assertTupleEqual((3, 4, 1), Grid.get_grid_dimensions(11))
self.assertTupleEqual((3, 4, 0), Grid.get_grid_dimensions(12))
self.assertTupleEqual((3, 5, 0), Grid.get_grid_dimensions(15))
self.assertTupleEqual((4, 4, 0), Grid.get_grid_dimensions(16))
self.assertTupleEqual((4, 6, 0), Grid.get_grid_dimensions(24))
self.assertTupleEqual((5, 5, 0), Grid.get_grid_dimensions(25))
self.assertTupleEqual((5, 6, 1), Grid.get_grid_dimensions(29))
self.assertTupleEqual((5, 7, 2), Grid.get_grid_dimensions(33))
class TestGridGeneration(Sc2TestBase):
options = {
"mission_order": options.MissionOrder.option_grid,
"excluded_missions": [mission_tables.SC2Mission.ZERO_HOUR.mission_name,],
"enabled_campaigns": {
SC2Campaign.WOL.campaign_name,
SC2Campaign.PROPHECY.campaign_name,
}
}
def test_size_matches_exclusions(self):
self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR.mission_name, self.multiworld.regions)
# WoL has 29 missions. -1 for Zero Hour being excluded, +1 for the automatically-added menu location
self.assertEqual(len(self.multiworld.regions), 29)

View File

@@ -0,0 +1,186 @@
import itertools
from dataclasses import fields
from random import Random
import unittest
from typing import List, Set, Iterable
from BaseClasses import ItemClassification, MultiWorld
import Options as CoreOptions
from .. import options, locations
from ..item import item_tables
from ..rules import SC2Logic
from ..mission_tables import SC2Race, MissionFlag, lookup_name_to_mission
class TestInventory:
"""
Runs checks against inventory with validation if all target items are progression and returns a random result
"""
def __init__(self) -> None:
self.random: Random = Random()
self.progression_types: Set[ItemClassification] = {ItemClassification.progression, ItemClassification.progression_skip_balancing}
def is_item_progression(self, item: str) -> bool:
return item_tables.item_table[item].classification in self.progression_types
def random_boolean(self):
return self.random.choice([True, False])
def has(self, item: str, player: int, count: int = 1):
if not self.is_item_progression(item):
raise AssertionError("Logic item {} is not a progression item".format(item))
return self.random_boolean()
def has_any(self, items: Set[str], player: int):
non_progression_items = [item for item in items if not self.is_item_progression(item)]
if len(non_progression_items) > 0:
raise AssertionError("Logic items {} are not progression items".format(non_progression_items))
return self.random_boolean()
def has_all(self, items: Set[str], player: int):
return self.has_any(items, player)
def has_group(self, item_group: str, player: int, count: int = 1):
return self.random_boolean()
def count_group(self, item_name_group: str, player: int) -> int:
return self.random.randrange(0, 20)
def count(self, item: str, player: int) -> int:
if not self.is_item_progression(item):
raise AssertionError("Item {} is not a progression item".format(item))
random_value: int = self.random.randrange(0, 5)
if random_value == 4: # 0-3 has a higher chance due to logic rules
return self.random.randrange(4, 100)
else:
return random_value
def count_from_list(self, items: Iterable[str], player: int) -> int:
return sum(self.count(item_name, player) for item_name in items)
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
return sum(self.count(item_name, player) for item_name in items)
class TestWorld:
"""
Mock world to simulate different player options for logic rules
"""
def __init__(self) -> None:
defaults = dict()
for field in fields(options.Starcraft2Options):
field_class = field.type
option_name = field.name
if isinstance(field_class, str):
if field_class in globals():
field_class = globals()[field_class]
else:
field_class = CoreOptions.__dict__[field.type]
defaults[option_name] = field_class(options.get_option_value(None, option_name))
self.options: options.Starcraft2Options = options.Starcraft2Options(**defaults)
self.options.mission_order.value = options.MissionOrder.option_vanilla_shuffled
self.player = 1
self.multiworld = MultiWorld(1)
class TestRules(unittest.TestCase):
def setUp(self) -> None:
self.required_tactics_values: List[int] = [
options.RequiredTactics.option_standard, options.RequiredTactics.option_advanced
]
self.all_in_map_values: List[int] = [
options.AllInMap.option_ground, options.AllInMap.option_air
]
self.take_over_ai_allies_values: List[int] = [
options.TakeOverAIAllies.option_true, options.TakeOverAIAllies.option_false
]
self.kerrigan_presence_values: List[int] = [
options.KerriganPresence.option_vanilla, options.KerriganPresence.option_not_present
]
self.NUM_TEST_RUNS = 100
@staticmethod
def _get_world(
required_tactics: int = options.RequiredTactics.default,
all_in_map: int = options.AllInMap.default,
take_over_ai_allies: int = options.TakeOverAIAllies.default,
kerrigan_presence: int = options.KerriganPresence.default,
# setting this to everywhere catches one extra logic check for Amon's Fall without missing any
spear_of_adun_passive_presence: int = options.SpearOfAdunPassiveAbilityPresence.option_everywhere,
) -> TestWorld:
test_world = TestWorld()
test_world.options.required_tactics.value = required_tactics
test_world.options.all_in_map.value = all_in_map
test_world.options.take_over_ai_allies.value = take_over_ai_allies
test_world.options.kerrigan_presence.value = kerrigan_presence
test_world.options.spear_of_adun_passive_ability_presence.value = spear_of_adun_passive_presence
test_world.logic = SC2Logic(test_world) # type: ignore
return test_world
def test_items_in_rules_are_progression(self):
test_inventory = TestInventory()
for option in self.required_tactics_values:
test_world = self._get_world(required_tactics=option)
location_data = locations.get_locations(test_world)
for location in location_data:
for _ in range(self.NUM_TEST_RUNS):
location.rule(test_inventory)
def test_items_in_all_in_are_progression(self):
test_inventory = TestInventory()
for test_options in itertools.product(self.required_tactics_values, self.all_in_map_values):
test_world = self._get_world(required_tactics=test_options[0], all_in_map=test_options[1])
for location in locations.get_locations(test_world):
if 'All-In' not in location.region:
continue
for _ in range(self.NUM_TEST_RUNS):
location.rule(test_inventory)
def test_items_in_kerriganless_missions_are_progression(self):
test_inventory = TestInventory()
for test_options in itertools.product(self.required_tactics_values, self.kerrigan_presence_values):
test_world = self._get_world(required_tactics=test_options[0], kerrigan_presence=test_options[1])
for location in locations.get_locations(test_world):
mission = lookup_name_to_mission[location.region]
if MissionFlag.Kerrigan not in mission.flags:
continue
for _ in range(self.NUM_TEST_RUNS):
location.rule(test_inventory)
def test_items_in_ai_takeover_missions_are_progression(self):
test_inventory = TestInventory()
for test_options in itertools.product(self.required_tactics_values, self.take_over_ai_allies_values):
test_world = self._get_world(required_tactics=test_options[0], take_over_ai_allies=test_options[1])
for location in locations.get_locations(test_world):
mission = lookup_name_to_mission[location.region]
if MissionFlag.AiAlly not in mission.flags:
continue
for _ in range(self.NUM_TEST_RUNS):
location.rule(test_inventory)
def test_items_in_hard_rules_are_progression(self):
test_inventory = TestInventory()
test_world = TestWorld()
test_world.options.required_tactics.value = options.RequiredTactics.option_any_units
test_world.logic = SC2Logic(test_world)
location_data = locations.get_locations(test_world)
for location in location_data:
if location.hard_rule is not None:
for _ in range(10):
location.hard_rule(test_inventory)
def test_items_in_any_units_rules_are_progression(self):
test_inventory = TestInventory()
test_world = TestWorld()
test_world.options.required_tactics.value = options.RequiredTactics.option_any_units
logic = SC2Logic(test_world)
test_world.logic = logic
for race in (SC2Race.TERRAN, SC2Race.PROTOSS, SC2Race.ZERG):
for target in range(1, 5):
rule = logic.has_race_units(target, race)
for _ in range(10):
rule(test_inventory)

View File

@@ -0,0 +1,492 @@
"""
Unit tests for yaml usecases we want to support
"""
from .test_base import Sc2SetupTestBase
from .. import get_all_missions, mission_tables, options
from ..item import item_groups, item_tables, item_names
from ..mission_tables import SC2Race, SC2Mission, SC2Campaign, MissionFlag
from ..options import EnabledCampaigns, MasteryLocations
class TestSupportedUseCases(Sc2SetupTestBase):
def test_vanilla_all_campaigns_generates(self) -> None:
world_options = {
'mission_order': options.MissionOrder.option_vanilla,
'enabled_campaigns': EnabledCampaigns.valid_keys,
}
self.generate_world(world_options)
world_regions = [region.name for region in self.multiworld.regions if region.name != "Menu"]
self.assertEqual(len(world_regions), 83, "Unexpected number of missions for vanilla mission order")
def test_terran_with_nco_units_only_generates(self):
world_options = {
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
SC2Campaign.NCO.campaign_name
},
'excluded_items': {
item_groups.ItemGroupNames.TERRAN_UNITS: 0,
},
'unexcluded_items': {
item_groups.ItemGroupNames.NCO_UNITS: 0,
},
'max_number_of_upgrades': 2,
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
world_item_names = [item.name for item in self.multiworld.itempool]
self.assertIn(item_names.MARINE, world_item_names)
self.assertIn(item_names.RAVEN, world_item_names)
self.assertIn(item_names.LIBERATOR, world_item_names)
self.assertIn(item_names.BATTLECRUISER, world_item_names)
self.assertNotIn(item_names.DIAMONDBACK, world_item_names)
self.assertNotIn(item_names.DIAMONDBACK_BURST_CAPACITORS, world_item_names)
self.assertNotIn(item_names.VIKING, world_item_names)
def test_nco_with_nobuilds_excluded_generates(self):
world_options = {
'enabled_campaigns': {
SC2Campaign.NCO.campaign_name
},
'shuffle_no_build': options.ShuffleNoBuild.option_false,
'mission_order': options.MissionOrder.option_mini_campaign,
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
missions = get_all_missions(self.world.custom_mission_order)
self.assertNotIn(mission_tables.SC2Mission.THE_ESCAPE, missions)
self.assertNotIn(mission_tables.SC2Mission.IN_THE_ENEMY_S_SHADOW, missions)
for mission in missions:
self.assertEqual(mission_tables.SC2Campaign.NCO, mission.campaign)
def test_terran_with_nco_upgrades_units_only_generates(self):
world_options = {
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
SC2Campaign.NCO.campaign_name
},
'mission_order': options.MissionOrder.option_vanilla_shuffled,
'excluded_items': {
item_groups.ItemGroupNames.TERRAN_ITEMS: 0,
},
'unexcluded_items': {
item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: 0,
item_groups.ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS: 1,
},
'excluded_missions': [
# These missions have trouble fulfilling Terran Power Rating under these terms
SC2Mission.SUPERNOVA.mission_name,
SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name,
SC2Mission.TROUBLE_IN_PARADISE.mission_name,
],
'mastery_locations': MasteryLocations.option_disabled,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool + self.multiworld.precollected_items[1]]
self.assertTrue(world_item_names)
missions = get_all_missions(self.world.custom_mission_order)
for mission in missions:
self.assertIn(mission_tables.MissionFlag.Terran, mission.flags)
self.assertIn(item_names.MARINE, world_item_names)
self.assertIn(item_names.MARAUDER, world_item_names)
self.assertIn(item_names.BUNKER, world_item_names)
self.assertIn(item_names.BANSHEE, world_item_names)
self.assertIn(item_names.BATTLECRUISER_ATX_LASER_BATTERY, world_item_names)
self.assertIn(item_names.NOVA_C20A_CANISTER_RIFLE, world_item_names)
self.assertGreaterEqual(world_item_names.count(item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS), 2)
self.assertGreaterEqual(world_item_names.count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON), 3)
self.assertNotIn(item_names.MEDIC, world_item_names)
self.assertNotIn(item_names.PSI_DISRUPTER, world_item_names)
self.assertNotIn(item_names.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS, world_item_names)
self.assertNotIn(item_names.HELLION_INFERNAL_PLATING, world_item_names)
self.assertNotIn(item_names.CELLULAR_REACTOR, world_item_names)
self.assertNotIn(item_names.TECH_REACTOR, world_item_names)
def test_nco_and_2_wol_missions_only_can_generate_with_vanilla_items_only(self) -> None:
world_options = {
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
SC2Campaign.NCO.campaign_name
},
'excluded_missions': [
mission.mission_name for mission in mission_tables.SC2Mission
if mission.campaign == mission_tables.SC2Campaign.WOL
and mission.mission_name not in (mission_tables.SC2Mission.LIBERATION_DAY.mission_name, mission_tables.SC2Mission.THE_OUTLAWS.mission_name)
],
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'mastery_locations': options.MasteryLocations.option_disabled,
'vanilla_items_only': True,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
self.assertTrue(item_names)
self.assertNotIn(item_names.LIBERATOR, world_item_names)
self.assertNotIn(item_names.MARAUDER_PROGRESSIVE_STIMPACK, world_item_names)
self.assertNotIn(item_names.HELLION_HELLBAT, world_item_names)
self.assertNotIn(item_names.BATTLECRUISER_CLOAK, world_item_names)
def test_free_protoss_only_generates(self) -> None:
world_options = {
'enabled_campaigns': {
SC2Campaign.PROPHECY.campaign_name,
SC2Campaign.PROLOGUE.campaign_name
},
# todo(mm): Currently, these settings don't generate on grid because there are not enough EASY missions
'mission_order': options.MissionOrder.option_vanilla_shuffled,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'accessibility': 'locations',
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
self.assertTrue(world_item_names)
missions = get_all_missions(self.world.custom_mission_order)
self.assertEqual(len(missions), 7, "Wrong number of missions in free protoss seed")
for mission in missions:
self.assertIn(mission.campaign, (mission_tables.SC2Campaign.PROLOGUE, mission_tables.SC2Campaign.PROPHECY))
for item_name in world_item_names:
self.assertIn(item_tables.item_table[item_name].race, (mission_tables.SC2Race.ANY, mission_tables.SC2Race.PROTOSS))
def test_resource_filler_items_may_be_put_in_start_inventory(self) -> None:
NUM_RESOURCE_ITEMS = 10
world_options = {
'start_inventory': {
item_names.STARTING_MINERALS: NUM_RESOURCE_ITEMS,
item_names.STARTING_VESPENE: NUM_RESOURCE_ITEMS,
item_names.STARTING_SUPPLY: NUM_RESOURCE_ITEMS,
},
}
self.generate_world(world_options)
start_item_names = [item.name for item in self.multiworld.precollected_items[self.player]]
self.assertEqual(start_item_names.count(item_names.STARTING_MINERALS), NUM_RESOURCE_ITEMS, "Wrong number of starting minerals in starting inventory")
self.assertEqual(start_item_names.count(item_names.STARTING_VESPENE), NUM_RESOURCE_ITEMS, "Wrong number of starting vespene in starting inventory")
self.assertEqual(start_item_names.count(item_names.STARTING_SUPPLY), NUM_RESOURCE_ITEMS, "Wrong number of starting supply in starting inventory")
def test_excluding_protoss_excludes_campaigns_and_items(self) -> None:
world_options = {
'selected_races': {
SC2Race.TERRAN.get_title(),
SC2Race.ZERG.get_title(),
},
'enabled_campaigns': options.EnabledCampaigns.valid_keys,
'mission_order': options.MissionOrder.option_grid,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
world_regions = [region.name for region in self.multiworld.regions]
world_regions.remove('Menu')
for item_name in world_item_names:
self.assertNotEqual(item_tables.item_table[item_name].race, mission_tables.SC2Race.PROTOSS, f"{item_name} is a PROTOSS item!")
for region in world_regions:
self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign,
(mission_tables.SC2Campaign.LOTV, mission_tables.SC2Campaign.PROPHECY, mission_tables.SC2Campaign.PROLOGUE),
f"{region} is a PROTOSS mission!")
def test_excluding_terran_excludes_campaigns_and_items(self) -> None:
world_options = {
'selected_races': {
SC2Race.ZERG.get_title(),
SC2Race.PROTOSS.get_title(),
},
'enabled_campaigns': EnabledCampaigns.valid_keys,
'mission_order': options.MissionOrder.option_grid,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
world_regions = [region.name for region in self.multiworld.regions]
world_regions.remove('Menu')
for item_name in world_item_names:
self.assertNotEqual(item_tables.item_table[item_name].race, mission_tables.SC2Race.TERRAN,
f"{item_name} is a TERRAN item!")
for region in world_regions:
self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign,
(mission_tables.SC2Campaign.WOL, mission_tables.SC2Campaign.NCO),
f"{region} is a TERRAN mission!")
def test_excluding_zerg_excludes_campaigns_and_items(self) -> None:
world_options = {
'selected_races': {
SC2Race.TERRAN.get_title(),
SC2Race.PROTOSS.get_title(),
},
'enabled_campaigns': EnabledCampaigns.valid_keys,
'mission_order': options.MissionOrder.option_grid,
'excluded_missions': [
SC2Mission.THE_INFINITE_CYCLE.mission_name
]
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
world_regions = [region.name for region in self.multiworld.regions]
world_regions.remove('Menu')
for item_name in world_item_names:
self.assertNotEqual(item_tables.item_table[item_name].race, mission_tables.SC2Race.ZERG,
f"{item_name} is a ZERG item!")
# have to manually exclude the only non-zerg HotS mission...
for region in filter(lambda region: region != "With Friends Like These", world_regions):
self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign,
([mission_tables.SC2Campaign.HOTS]),
f"{region} is a ZERG mission!")
def test_excluding_faction_on_vanilla_order_excludes_epilogue(self) -> None:
world_options = {
'selected_races': {
SC2Race.TERRAN.get_title(),
SC2Race.PROTOSS.get_title(),
},
'enabled_campaigns': EnabledCampaigns.valid_keys,
'mission_order': options.MissionOrder.option_vanilla,
}
self.generate_world(world_options)
world_regions = [region.name for region in self.multiworld.regions]
world_regions.remove('Menu')
for region in world_regions:
self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign,
([mission_tables.SC2Campaign.EPILOGUE]),
f"{region} is an epilogue mission!")
def test_race_swap_pick_one_has_correct_length_and_includes_swaps(self) -> None:
world_options = {
'selected_races': options.SelectRaces.valid_keys,
'enable_race_swap': options.EnableRaceSwapVariants.option_pick_one,
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
},
'mission_order': options.MissionOrder.option_grid,
'excluded_missions': [mission_tables.SC2Mission.ZERO_HOUR.mission_name],
}
self.generate_world(world_options)
world_regions = [region.name for region in self.multiworld.regions]
world_regions.remove('Menu')
NUM_WOL_MISSIONS = len([mission for mission in SC2Mission if mission.campaign == SC2Campaign.WOL and MissionFlag.RaceSwap not in mission.flags])
races = set(mission_tables.lookup_name_to_mission[mission].race for mission in world_regions)
self.assertEqual(len(world_regions), NUM_WOL_MISSIONS)
self.assertTrue(SC2Race.ZERG in races or SC2Race.PROTOSS in races)
def test_start_inventory_upgrade_level_includes_only_correct_bundle(self) -> None:
world_options = {
'start_inventory': {
item_groups.ItemGroupNames.TERRAN_GENERIC_UPGRADES: 1,
},
'locked_items': {
# One unit of each class to guarantee upgrades are available
item_names.MARINE: 1,
item_names.VULTURE: 1,
item_names.BANSHEE: 1,
},
'generic_upgrade_items': options.GenericUpgradeItems.option_bundle_unit_class,
'selected_races': {
SC2Race.TERRAN.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_disabled,
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
},
'mission_order': options.MissionOrder.option_grid,
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
world_item_names = [item.name for item in self.multiworld.itempool]
start_inventory = [item.name for item in self.multiworld.precollected_items[self.player]]
# Start inventory
self.assertIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, start_inventory)
self.assertIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE, start_inventory)
self.assertIn(item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE, start_inventory)
# Additional items in pool -- standard tactics will require additional levels
self.assertIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, world_item_names)
self.assertIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE, world_item_names)
self.assertIn(item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE, world_item_names)
def test_kerrigan_max_active_abilities(self):
target_number: int = 8
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.ZERG.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'kerrigan_max_active_abilities': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
kerrigan_actives = [item_name for item_name in world_item_names if item_name in item_groups.kerrigan_active_abilities]
self.assertLessEqual(len(kerrigan_actives), target_number)
def test_kerrigan_max_passive_abilities(self):
target_number: int = 3
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.ZERG.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'kerrigan_max_passive_abilities': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
kerrigan_passives = [item_name for item_name in world_item_names if item_name in item_groups.kerrigan_passives]
self.assertLessEqual(len(kerrigan_passives), target_number)
def test_spear_of_adun_max_active_abilities(self):
target_number: int = 8
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.PROTOSS.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'spear_of_adun_max_active_abilities': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_calldowns]
self.assertLessEqual(len(spear_of_adun_actives), target_number)
def test_spear_of_adun_max_autocasts(self):
target_number: int = 2
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.PROTOSS.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'spear_of_adun_max_passive_abilities': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
spear_of_adun_autocasts = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_castable_passives]
self.assertLessEqual(len(spear_of_adun_autocasts), target_number)
def test_nova_max_weapons(self):
target_number: int = 3
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.TERRAN.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'nova_max_weapons': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
nova_weapons = [item_name for item_name in world_item_names if item_name in item_groups.nova_weapons]
self.assertLessEqual(len(nova_weapons), target_number)
def test_nova_max_gadgets(self):
target_number: int = 3
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.TERRAN.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'nova_max_gadgets': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
nova_gadgets = [item_name for item_name in world_item_names if item_name in item_groups.nova_gadgets]
self.assertLessEqual(len(nova_gadgets), target_number)
def test_mercs_only(self) -> None:
world_options = {
'selected_races': [
SC2Race.TERRAN.get_title(),
SC2Race.ZERG.get_title(),
],
'required_tactics': options.RequiredTactics.option_any_units,
'excluded_items': {
item_groups.ItemGroupNames.TERRAN_UNITS: 0,
item_groups.ItemGroupNames.ZERG_UNITS: 0,
},
'unexcluded_items': {
item_groups.ItemGroupNames.TERRAN_MERCENARIES: 0,
item_groups.ItemGroupNames.ZERG_MERCENARIES: 0,
},
'start_inventory': {
item_names.PROGRESSIVE_FAST_DELIVERY: 1,
item_names.ROGUE_FORCES: 1,
item_names.UNRESTRICTED_MUTATION: 1,
item_names.EVOLUTIONARY_LEAP: 1,
},
'mission_order': options.MissionOrder.option_grid,
'excluded_missions': [
SC2Mission.ENEMY_WITHIN.mission_name, # Requires a unit for Niadra to build
],
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
terran_nonmerc_units = tuple(
item_name
for item_name in world_item_names
if item_name in item_groups.terran_units and item_name not in item_groups.terran_mercenaries
)
zerg_nonmerc_units = tuple(
item_name
for item_name in world_item_names
if item_name in item_groups.zerg_units and item_name not in item_groups.zerg_mercenaries
)
self.assertTupleEqual(terran_nonmerc_units, ())
self.assertTupleEqual(zerg_nonmerc_units, ())

View File

@@ -0,0 +1,38 @@
from typing import Dict, List
"""
This file is for handling SC2 data read via the bot
"""
normalized_unit_types: Dict[str, str] = {
# Thor morphs
"AP_ThorAP": "AP_Thor",
"AP_MercThorAP": "AP_MercThor",
"AP_ThorMengskSieged": "AP_ThorMengsk",
"AP_ThorMengskAP": "AP_ThorMengsk",
# Siege Tank morphs
"AP_SiegeTankSiegedTransportable": "AP_SiegeTank",
"AP_SiegeTankMengskSiegedTransportable": "AP_SiegeTankMengsk",
"AP_SiegeBreakerSiegedTransportable": "AP_SiegeBreaker",
"AP_InfestedSiegeBreakerSiegedTransportable": "AP_InfestedSiegeBreaker",
"AP_StukovInfestedSiegeTank": "AP_StukovInfestedSiegeTankUprooted",
# Cargo size upgrades
"AP_FirebatOptimizedLogistics": "AP_Firebat",
"AP_DevilDogOptimizedLogistics": "AP_DevilDog",
"AP_GhostResourceEfficiency": "AP_Ghost",
"AP_GhostMengskResourceEfficiency": "AP_GhostMengsk",
"AP_SpectreResourceEfficiency": "AP_Spectre",
"AP_UltraliskResourceEfficiency": "AP_Ultralisk",
"AP_MercUltraliskResourceEfficiency": "AP_MercUltralisk",
"AP_ReaperResourceEfficiency": "AP_Reaper",
"AP_MercReaperResourceEfficiency": "AP_MercReaper",
}
worker_units: List[str] = [
"AP_SCV",
"AP_MULE", # Mules can't currently build (or be traded due to timed life), this is future proofing just in case
"AP_Drone",
"AP_SISCV", # Infested SCV
"AP_Probe",
"AP_ElderProbe",
]