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:
1630
worlds/sc2/Client.py
1630
worlds/sc2/Client.py
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -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,
|
||||
]
|
||||
@@ -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"
|
||||
2554
worlds/sc2/Items.py
2554
worlds/sc2/Items.py
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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]
|
||||
@@ -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,
|
||||
]
|
||||
@@ -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()])
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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
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
655
worlds/sc2/client_gui.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
1092
worlds/sc2/docs/custom_mission_orders_en.md
Normal file
1092
worlds/sc2/docs/custom_mission_orders_en.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
98
worlds/sc2/gui_config.py
Normal 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
173
worlds/sc2/item/__init__.py
Normal 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
|
||||
178
worlds/sc2/item/item_annotations.py
Normal file
178
worlds/sc2/item/item_annotations.py
Normal 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)",
|
||||
}
|
||||
1127
worlds/sc2/item/item_descriptions.py
Normal file
1127
worlds/sc2/item/item_descriptions.py
Normal file
File diff suppressed because it is too large
Load Diff
902
worlds/sc2/item/item_groups.py
Normal file
902
worlds/sc2/item/item_groups.py
Normal 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()
|
||||
]
|
||||
957
worlds/sc2/item/item_names.py
Normal file
957
worlds/sc2/item/item_names.py
Normal 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"
|
||||
]
|
||||
266
worlds/sc2/item/item_parents.py
Normal file
266
worlds/sc2/item/item_parents.py
Normal 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()
|
||||
2415
worlds/sc2/item/item_tables.py
Normal file
2415
worlds/sc2/item/item_tables.py
Normal file
File diff suppressed because it is too large
Load Diff
57
worlds/sc2/item/parent_names.py
Normal file
57
worlds/sc2/item/parent_names.py
Normal 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"
|
||||
40
worlds/sc2/location_groups.py
Normal file
40
worlds/sc2/location_groups.py
Normal 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
14175
worlds/sc2/locations.py
Normal file
File diff suppressed because it is too large
Load Diff
194
worlds/sc2/mission_groups.py
Normal file
194
worlds/sc2/mission_groups.py
Normal 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
|
||||
]
|
||||
66
worlds/sc2/mission_order/__init__.py
Normal file
66
worlds/sc2/mission_order/__init__.py
Normal 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()
|
||||
|
||||
389
worlds/sc2/mission_order/entry_rules.py
Normal file
389
worlds/sc2/mission_order/entry_rules.py
Normal 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()
|
||||
)
|
||||
702
worlds/sc2/mission_order/generation.py
Normal file
702
worlds/sc2/mission_order/generation.py
Normal 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)
|
||||
620
worlds/sc2/mission_order/layout_types.py
Normal file
620
worlds/sc2/mission_order/layout_types.py
Normal 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)])
|
||||
251
worlds/sc2/mission_order/mission_pools.py
Normal file
251
worlds/sc2/mission_order/mission_pools.py
Normal 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
|
||||
606
worlds/sc2/mission_order/nodes.py
Normal file
606
worlds/sc2/mission_order/nodes.py
Normal 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,
|
||||
)
|
||||
472
worlds/sc2/mission_order/options.py
Normal file
472
worlds/sc2/mission_order/options.py
Normal 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
|
||||
|
||||
164
worlds/sc2/mission_order/presets_scripted.py
Normal file
164
worlds/sc2/mission_order/presets_scripted.py
Normal 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
|
||||
916
worlds/sc2/mission_order/presets_static.py
Normal file
916
worlds/sc2/mission_order/presets_static.py
Normal 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
|
||||
53
worlds/sc2/mission_order/slot_data.py
Normal file
53
worlds/sc2/mission_order/slot_data.py
Normal 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)
|
||||
577
worlds/sc2/mission_tables.py
Normal file
577
worlds/sc2/mission_tables.py
Normal 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
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
493
worlds/sc2/pool_filter.py
Normal 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
532
worlds/sc2/regions.py
Normal 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
3582
worlds/sc2/rules.py
Normal file
File diff suppressed because it is too large
Load Diff
49
worlds/sc2/settings.py
Normal file
49
worlds/sc2/settings.py
Normal 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
61
worlds/sc2/starcraft2.kv
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
216
worlds/sc2/test/test_custom_mission_orders.py
Normal file
216
worlds/sc2/test/test_custom_mission_orders.py
Normal 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)
|
||||
1228
worlds/sc2/test/test_generation.py
Normal file
1228
worlds/sc2/test/test_generation.py
Normal file
File diff suppressed because it is too large
Load Diff
88
worlds/sc2/test/test_item_filtering.py
Normal file
88
worlds/sc2/test/test_item_filtering.py
Normal 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)
|
||||
|
||||
18
worlds/sc2/test/test_itemdescriptions.py
Normal file
18
worlds/sc2/test/test_itemdescriptions.py
Normal 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}")
|
||||
32
worlds/sc2/test/test_itemgroups.py
Normal file
32
worlds/sc2/test/test_itemgroups.py
Normal 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)
|
||||
170
worlds/sc2/test/test_items.py
Normal file
170
worlds/sc2/test/test_items.py
Normal 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)
|
||||
|
||||
37
worlds/sc2/test/test_location_groups.py
Normal file
37
worlds/sc2/test/test_location_groups.py
Normal 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'])
|
||||
9
worlds/sc2/test/test_mission_groups.py
Normal file
9
worlds/sc2/test/test_mission_groups.py
Normal 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])
|
||||
@@ -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()))
|
||||
|
||||
40
worlds/sc2/test/test_regions.py
Normal file
40
worlds/sc2/test/test_regions.py
Normal 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)
|
||||
186
worlds/sc2/test/test_rules.py
Normal file
186
worlds/sc2/test/test_rules.py
Normal 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)
|
||||
|
||||
|
||||
492
worlds/sc2/test/test_usecases.py
Normal file
492
worlds/sc2/test/test_usecases.py
Normal 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, ())
|
||||
38
worlds/sc2/transfer_data.py
Normal file
38
worlds/sc2/transfer_data.py
Normal 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",
|
||||
]
|
||||
Reference in New Issue
Block a user