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:
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
|
||||
Reference in New Issue
Block a user