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
267 lines
13 KiB
Python
267 lines
13 KiB
Python
"""
|
|
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()
|