Files
Grinch-AP/worlds/sc2/mission_order/presets_scripted.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

165 lines
7.3 KiB
Python

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