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
607 lines
25 KiB
Python
607 lines
25 KiB
Python
"""
|
|
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,
|
|
)
|