Files
Grinch-AP/worlds/sc2/item/item_parents.py
Ziktofel 5f1835c546 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
2025-09-02 17:40:58 +02:00

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()