SC2: Multi-campaign (#2954)

Adds HotS, LotV and NCO campaigns to SC2 game.
The world's name has changed to reflect that (it's not only Wings of Liberty now)
The client was patched in a way that can still join to games generated prior this change
---------

Co-authored-by: Magnemania <magnemight@gmail.com>
Co-authored-by: EnvyDragon <138727357+EnvyDragon@users.noreply.github.com>
Co-authored-by: Matthew <matthew.marinets@gmail.com>
Co-authored-by: hopop201 <benjy.hopop201@gmail.com>
Co-authored-by: Salzkorn <salzkitty@gmail.com>
Co-authored-by: genderdruid <pallyoffail@gmail.com>
Co-authored-by: MadiMadsen <137329235+MadiMadsen@users.noreply.github.com>
Co-authored-by: neocerber <neocerber@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
This commit is contained in:
Ziktofel 2024-03-15 17:33:03 +01:00 committed by GitHub
parent ed9cbfecb9
commit e0e9fdd86a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
97 changed files with 13708 additions and 4739 deletions

View File

@ -25,7 +25,7 @@ Currently, the following games are supported:
* Hollow Knight
* The Witness
* Sonic Adventure 2: Battle
* Starcraft 2: Wings of Liberty
* Starcraft 2
* Donkey Kong Country 3
* Dark Souls 3
* Super Mario World

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import ModuleUpdate
ModuleUpdate.update()
from worlds.sc2wol.Client import launch
from worlds.sc2.Client import launch
import Utils
if __name__ == "__main__":

View File

@ -25,16 +25,16 @@ window.addEventListener('load', () => {
// Collapsible advancement sections
const categories = document.getElementsByClassName("location-category");
for (let i = 0; i < categories.length; i++) {
let hide_id = categories[i].id.split('-')[0];
if (hide_id == 'Total') {
for (let category of categories) {
let hide_id = category.id.split('_')[0];
if (hide_id === 'Total') {
continue;
}
categories[i].addEventListener('click', function() {
category.addEventListener('click', function() {
// Toggle the advancement list
document.getElementById(hide_id).classList.toggle("hide");
// Change text of the header
const tab_header = document.getElementById(hide_id+'-header').children[0];
const tab_header = document.getElementById(hide_id+'_header').children[0];
const orig_text = tab_header.innerHTML;
let new_text;
if (orig_text.includes("▼")) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,160 @@
#player-tracker-wrapper{
margin: 0;
}
#tracker-table td {
vertical-align: top;
}
.inventory-table-area{
border: 2px solid #000000;
border-radius: 4px;
padding: 3px 10px 3px 10px;
}
.inventory-table-area:has(.inventory-table-terran) {
width: 690px;
background-color: #525494;
}
.inventory-table-area:has(.inventory-table-zerg) {
width: 360px;
background-color: #9d60d2;
}
.inventory-table-area:has(.inventory-table-protoss) {
width: 400px;
background-color: #d2b260;
}
#tracker-table .inventory-table td{
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
}
.inventory-table td.title{
padding-top: 10px;
height: 20px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
}
.inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
border: 1px solid #000000;
filter: grayscale(100%) contrast(75%) brightness(20%);
background-color: black;
}
.inventory-table img.acquired{
filter: none;
background-color: black;
}
.inventory-table .tint-terran img.acquired {
filter: sepia(100%) saturate(300%) brightness(130%) hue-rotate(120deg)
}
.inventory-table .tint-protoss img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(180deg)
}
.inventory-table .tint-level-1 img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg)
}
.inventory-table .tint-level-2 img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(120deg)
}
.inventory-table .tint-level-3 img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(240deg)
}
.inventory-table div.counted-item {
position: relative;
}
.inventory-table div.item-count {
width: 160px;
text-align: left;
color: black;
font-family: "JuraBook", monospace;
font-weight: bold;
}
#location-table{
border: 2px solid #000000;
border-radius: 4px;
background-color: #87b678;
padding: 10px 3px 3px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
cursor: default;
}
#location-table table{
width: 100%;
}
#location-table th{
vertical-align: middle;
text-align: left;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
line-height: 20px;
}
#location-table td.counter {
text-align: right;
font-size: 14px;
}
#location-table td.toggle-arrow {
text-align: right;
}
#location-table tr#Total-header {
font-weight: bold;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}
#location-table tbody.locations {
font-size: 16px;
}
#location-table td.location-name {
padding-left: 16px;
}
#location-table td:has(.location-column) {
vertical-align: top;
}
#location-table .location-column {
width: 100%;
height: 100%;
}
#location-table .location-column .spacer {
min-height: 24px;
}
.hide {
display: none;
}

View File

@ -1,112 +0,0 @@
#player-tracker-wrapper{
margin: 0;
}
#inventory-table{
border-top: 2px solid #000000;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 710px;
background-color: #525494;
}
#inventory-table td{
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
}
#inventory-table td.title{
padding-top: 10px;
height: 20px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
}
#inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
border: 1px solid #000000;
filter: grayscale(100%) contrast(75%) brightness(20%);
background-color: black;
}
#inventory-table img.acquired{
filter: none;
background-color: black;
}
#inventory-table div.counted-item {
position: relative;
}
#inventory-table div.item-count {
text-align: left;
color: black;
font-family: "JuraBook", monospace;
font-weight: bold;
}
#location-table{
width: 710px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #525494;
padding: 10px 3px 3px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
cursor: default;
}
#location-table th{
vertical-align: middle;
text-align: left;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
line-height: 20px;
}
#location-table td.counter {
text-align: right;
font-size: 14px;
}
#location-table td.toggle-arrow {
text-align: right;
}
#location-table tr#Total-header {
font-weight: bold;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}
#location-table tbody.locations {
font-size: 16px;
}
#location-table td.location-name {
padding-left: 16px;
}
.hide {
display: none;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,366 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/sc2wolTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/sc2wolTracker.js') }}"></script>
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/jura" type="text/css"/>
</head>
<body>
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
<div style="margin-bottom: 0.5rem">
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
</div>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table">
<tr>
<td colspan="15" class="title">
Starting Resources
</td>
</tr>
<tr>
<td><img src="{{ icons['Starting Minerals'] }}" class="{{ 'acquired' if '+15 Starting Minerals' in acquired_items }}" title="Starting Minerals" /></td>
<td colspan="2"><div class="item-count">+{{ minerals_count }}</div></td>
<td><img src="{{ icons['Starting Vespene'] }}" class="{{ 'acquired' if '+15 Starting Vespene' in acquired_items }}" title="Starting Vespene" /></td>
<td colspan="2"><div class="item-count">+{{ vespene_count }}</div></td>
<!--
<td><img src="{{ icons['Starting Supply'] }}" class="{{ 'acquired' if '+2 Starting Supply' in acquired_items }}" title="Starting Supply" /></td>
<td colspan="2"><div class="item-count">+{{ supply_count }}</div></td>
-->
</tr>
<tr>
<td colspan="15" class="title">
Weapon & Armor Upgrades
</td>
</tr>
<tr>
<td><img src="{{ infantry_weapon_url }}" class="{{ 'acquired' if 'Progressive Infantry Weapon' in acquired_items }}" title="Progressive Infantry Weapons{% if infantry_weapon_level > 0 %} (Level {{ infantry_weapon_level }}){% endif %}" /></td>
<td><img src="{{ infantry_armor_url }}" class="{{ 'acquired' if 'Progressive Infantry Armor' in acquired_items }}" title="Progressive Infantry Armor{% if infantry_armor_level > 0 %} (Level {{ infantry_armor_level }}){% endif %}" /></td>
<td><img src="{{ vehicle_weapon_url }}" class="{{ 'acquired' if 'Progressive Vehicle Weapon' in acquired_items }}" title="Progressive Vehicle Weapons{% if vehicle_weapon_level > 0 %} (Level {{ vehicle_weapon_level }}){% endif %}" /></td>
<td><img src="{{ vehicle_armor_url }}" class="{{ 'acquired' if 'Progressive Vehicle Armor' in acquired_items }}" title="Progressive Vehicle Armor{% if vehicle_armor_level > 0 %} (Level {{ vehicle_armor_level }}){% endif %}" /></td>
<td><img src="{{ ship_weapon_url }}" class="{{ 'acquired' if 'Progressive Ship Weapon' in acquired_items }}" title="Progressive Ship Weapons{% if ship_weapon_level > 0 %} (Level {{ ship_weapon_level }}){% endif %}" /></td>
<td><img src="{{ ship_armor_url }}" class="{{ 'acquired' if 'Progressive Ship Armor' in acquired_items }}" title="Progressive Ship Armor{% if ship_armor_level > 0 %} (Level {{ ship_armor_level }}){% endif %}" /></td>
<td colspan="2"></td>
<td><img src="{{ icons['Ultra-Capacitors'] }}" class="{{ 'acquired' if 'Ultra-Capacitors' in acquired_items }}" title="Ultra-Capacitors" /></td>
<td><img src="{{ icons['Vanadium Plating'] }}" class="{{ 'acquired' if 'Vanadium Plating' in acquired_items }}" title="Vanadium Plating" /></td>
</tr>
<tr>
<td colspan="15" class="title">
Base
</td>
</tr>
<tr>
<td><img src="{{ icons['Bunker'] }}" class="{{ 'acquired' if 'Bunker' in acquired_items }}" title="Bunker" /></td>
<td><img src="{{ icons['Projectile Accelerator (Bunker)'] }}" class="{{ 'acquired' if 'Projectile Accelerator (Bunker)' in acquired_items }}" title="Projectile Accelerator (Bunker)" /></td>
<td><img src="{{ icons['Neosteel Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Neosteel Bunker (Bunker)' in acquired_items }}" title="Neosteel Bunker (Bunker)" /></td>
<td><img src="{{ icons['Shrike Turret (Bunker)'] }}" class="{{ 'acquired' if 'Shrike Turret (Bunker)' in acquired_items }}" title="Shrike Turret (Bunker)" /></td>
<td><img src="{{ icons['Fortified Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Fortified Bunker (Bunker)' in acquired_items }}" title="Fortified Bunker (Bunker)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Missile Turret'] }}" class="{{ 'acquired' if 'Missile Turret' in acquired_items }}" title="Missile Turret" /></td>
<td><img src="{{ icons['Titanium Housing (Missile Turret)'] }}" class="{{ 'acquired' if 'Titanium Housing (Missile Turret)' in acquired_items }}" title="Titanium Housing (Missile Turret)" /></td>
<td><img src="{{ icons['Hellstorm Batteries (Missile Turret)'] }}" class="{{ 'acquired' if 'Hellstorm Batteries (Missile Turret)' in acquired_items }}" title="Hellstorm Batteries (Missile Turret)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Orbital Command (Building)'] }}" class="{{ 'acquired' if 'Orbital Command (Building)' in acquired_items }}" title="Orbital Command (Building)" /></td>
<td><img src="{{ icons['Command Center Reactor'] }}" class="{{ 'acquired' if 'Command Center Reactor' in acquired_items }}" title="Command Center Reactor" /></td>
<td><img src="{{ icons['Planetary Fortress'] }}" class="{{ 'acquired' if 'Planetary Fortress' in acquired_items }}" title="Planetary Fortress" /></td>
<td></td>
<td><img src="{{ icons['Advanced Construction (SCV)'] }}" class="{{ 'acquired' if 'Advanced Construction (SCV)' in acquired_items }}" title="Advanced Construction (SCV)" /></td>
<td><img src="{{ icons['Dual-Fusion Welders (SCV)'] }}" class="{{ 'acquired' if 'Dual-Fusion Welders (SCV)' in acquired_items }}" title="Dual-Fusion Welders (SCV)" /></td>
<td></td>
<td><img src="{{ icons['Micro-Filtering'] }}" class="{{ 'acquired' if 'Micro-Filtering' in acquired_items }}" title="Micro-Filtering" /></td>
<td><img src="{{ icons['Automated Refinery'] }}" class="{{ 'acquired' if 'Automated Refinery' in acquired_items }}" title="Automated Refinery" /></td>
<td></td>
<td><img src="{{ icons['Tech Reactor'] }}" class="{{ 'acquired' if 'Tech Reactor' in acquired_items }}" title="Tech Reactor" /></td>
<td></td>
<td><img src="{{ icons['Orbital Depots'] }}" class="{{ 'acquired' if 'Orbital Depots' in acquired_items }}" title="Orbital Depots" /></td>
</tr>
<tr>
<td><img src="{{ icons['Sensor Tower'] }}" class="{{ 'acquired' if 'Sensor Tower' in acquired_items }}" title="Sensor Tower" /></td>
<td></td>
<td><img src="{{ icons['Perdition Turret'] }}" class="{{ 'acquired' if 'Perdition Turret' in acquired_items }}" title="Perdition Turret" /></td>
<td></td>
<td><img src="{{ icons['Hive Mind Emulator'] }}" class="{{ 'acquired' if 'Hive Mind Emulator' in acquired_items }}" title="Hive Mind Emulator" /></td>
<td></td>
<td><img src="{{ icons['Psi Disrupter'] }}" class="{{ 'acquired' if 'Psi Disrupter' in acquired_items }}" title="Psi Disrupter" /></td>
</tr>
<tr>
<td colspan="7" class="title">
Infantry
</td>
<td></td>
<td colspan="7" class="title">
Vehicles
</td>
</tr>
<tr>
<td><img src="{{ icons['Marine'] }}" class="{{ 'acquired' if 'Marine' in acquired_items }}" title="Marine" /></td>
<td><img src="{{ stimpack_marine_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marine)' in acquired_items }}" title="{{ stimpack_marine_name }}" /></td>
<td><img src="{{ icons['Combat Shield (Marine)'] }}" class="{{ 'acquired' if 'Combat Shield (Marine)' in acquired_items }}" title="Combat Shield (Marine)" /></td>
<td><img src="{{ icons['Laser Targeting System (Marine)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marine)' in acquired_items }}" title="Laser Targeting System (Marine)" /></td>
<td><img src="{{ icons['Magrail Munitions (Marine)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marine)' in acquired_items }}" title="Magrail Munitions (Marine)" /></td>
<td><img src="{{ icons['Optimized Logistics (Marine)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Marine)' in acquired_items }}" title="Optimized Logistics (Marine)" /></td>
<td colspan="2"></td>
<td><img src="{{ icons['Hellion'] }}" class="{{ 'acquired' if 'Hellion' in acquired_items }}" title="Hellion" /></td>
<td><img src="{{ icons['Twin-Linked Flamethrower (Hellion)'] }}" class="{{ 'acquired' if 'Twin-Linked Flamethrower (Hellion)' in acquired_items }}" title="Twin-Linked Flamethrower (Hellion)" /></td>
<td><img src="{{ icons['Thermite Filaments (Hellion)'] }}" class="{{ 'acquired' if 'Thermite Filaments (Hellion)' in acquired_items }}" title="Thermite Filaments (Hellion)" /></td>
<td><img src="{{ icons['Hellbat Aspect (Hellion)'] }}" class="{{ 'acquired' if 'Hellbat Aspect (Hellion)' in acquired_items }}" title="Hellbat Aspect (Hellion)" /></td>
<td><img src="{{ icons['Smart Servos (Hellion)'] }}" class="{{ 'acquired' if 'Smart Servos (Hellion)' in acquired_items }}" title="Smart Servos (Hellion)" /></td>
<td><img src="{{ icons['Optimized Logistics (Hellion)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Hellion)' in acquired_items }}" title="Optimized Logistics (Hellion)" /></td>
<td><img src="{{ icons['Jump Jets (Hellion)'] }}" class="{{ 'acquired' if 'Jump Jets (Hellion)' in acquired_items }}" title="Jump Jets (Hellion)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Medic'] }}" class="{{ 'acquired' if 'Medic' in acquired_items }}" title="Medic" /></td>
<td><img src="{{ icons['Advanced Medic Facilities (Medic)'] }}" class="{{ 'acquired' if 'Advanced Medic Facilities (Medic)' in acquired_items }}" title="Advanced Medic Facilities (Medic)" /></td>
<td><img src="{{ icons['Stabilizer Medpacks (Medic)'] }}" class="{{ 'acquired' if 'Stabilizer Medpacks (Medic)' in acquired_items }}" title="Stabilizer Medpacks (Medic)" /></td>
<td><img src="{{ icons['Restoration (Medic)'] }}" class="{{ 'acquired' if 'Restoration (Medic)' in acquired_items }}" title="Restoration (Medic)" /></td>
<td><img src="{{ icons['Optical Flare (Medic)'] }}" class="{{ 'acquired' if 'Optical Flare (Medic)' in acquired_items }}" title="Optical Flare (Medic)" /></td>
<td><img src="{{ icons['Optimized Logistics (Medic)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Medic)' in acquired_items }}" title="Optimized Logistics (Medic)" /></td>
<td colspan="2"></td>
<td></td>
<td><img src="{{ stimpack_hellion_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Hellion)' in acquired_items }}" title="{{ stimpack_hellion_name }}" /></td>
</tr>
<tr>
<td><img src="{{ icons['Firebat'] }}" class="{{ 'acquired' if 'Firebat' in acquired_items }}" title="Firebat" /></td>
<td><img src="{{ icons['Incinerator Gauntlets (Firebat)'] }}" class="{{ 'acquired' if 'Incinerator Gauntlets (Firebat)' in acquired_items }}" title="Incinerator Gauntlets (Firebat)" /></td>
<td><img src="{{ icons['Juggernaut Plating (Firebat)'] }}" class="{{ 'acquired' if 'Juggernaut Plating (Firebat)' in acquired_items }}" title="Juggernaut Plating (Firebat)" /></td>
<td><img src="{{ stimpack_firebat_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Firebat)' in acquired_items }}" title="{{ stimpack_firebat_name }}" /></td>
<td><img src="{{ icons['Optimized Logistics (Firebat)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Firebat)' in acquired_items }}" title="Optimized Logistics (Firebat)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></td>
<td><img src="{{ icons['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (Vulture)" /></td>
<td><img src="{{ icons['Ion Thrusters (Vulture)'] }}" class="{{ 'acquired' if 'Ion Thrusters (Vulture)' in acquired_items }}" title="Ion Thrusters (Vulture)" /></td>
<td><img src="{{ icons['Auto Launchers (Vulture)'] }}" class="{{ 'acquired' if 'Auto Launchers (Vulture)' in acquired_items }}" title="Auto Launchers (Vulture)" /></td>
<td></td>
<td><img src="{{ icons['Cerberus Mine (Spider Mine)'] }}" class="{{ 'acquired' if 'Cerberus Mine (Spider Mine)' in acquired_items }}" title="Cerberus Mine (Spider Mine)" /></td>
<td><img src="{{ icons['High Explosive Munition (Spider Mine)'] }}" class="{{ 'acquired' if 'High Explosive Munition (Spider Mine)' in acquired_items }}" title="High Explosive Munition (Spider Mine)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Marauder'] }}" class="{{ 'acquired' if 'Marauder' in acquired_items }}" title="Marauder" /></td>
<td><img src="{{ icons['Concussive Shells (Marauder)'] }}" class="{{ 'acquired' if 'Concussive Shells (Marauder)' in acquired_items }}" title="Concussive Shells (Marauder)" /></td>
<td><img src="{{ icons['Kinetic Foam (Marauder)'] }}" class="{{ 'acquired' if 'Kinetic Foam (Marauder)' in acquired_items }}" title="Kinetic Foam (Marauder)" /></td>
<td><img src="{{ stimpack_marauder_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marauder)' in acquired_items }}" title="{{ stimpack_marauder_name }}" /></td>
<td><img src="{{ icons['Laser Targeting System (Marauder)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marauder)' in acquired_items }}" title="Laser Targeting System (Marauder)" /></td>
<td><img src="{{ icons['Magrail Munitions (Marauder)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marauder)' in acquired_items }}" title="Magrail Munitions (Marauder)" /></td>
<td><img src="{{ icons['Internal Tech Module (Marauder)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Marauder)' in acquired_items }}" title="Internal Tech Module (Marauder)" /></td>
<td></td>
<td><img src="{{ icons['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></td>
<td><img src="{{ icons['Multi-Lock Weapons System (Goliath)'] }}" class="{{ 'acquired' if 'Multi-Lock Weapons System (Goliath)' in acquired_items }}" title="Multi-Lock Weapons System (Goliath)" /></td>
<td><img src="{{ icons['Ares-Class Targeting System (Goliath)'] }}" class="{{ 'acquired' if 'Ares-Class Targeting System (Goliath)' in acquired_items }}" title="Ares-Class Targeting System (Goliath)" /></td>
<td><img src="{{ icons['Jump Jets (Goliath)'] }}" class="{{ 'acquired' if 'Jump Jets (Goliath)' in acquired_items }}" title="Jump Jets (Goliath)" /></td>
<td><img src="{{ icons['Optimized Logistics (Goliath)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Goliath)' in acquired_items }}" title="Optimized Logistics (Goliath)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Reaper'] }}" class="{{ 'acquired' if 'Reaper' in acquired_items }}" title="Reaper" /></td>
<td><img src="{{ icons['U-238 Rounds (Reaper)'] }}" class="{{ 'acquired' if 'U-238 Rounds (Reaper)' in acquired_items }}" title="U-238 Rounds (Reaper)" /></td>
<td><img src="{{ icons['G-4 Clusterbomb (Reaper)'] }}" class="{{ 'acquired' if 'G-4 Clusterbomb (Reaper)' in acquired_items }}" title="G-4 Clusterbomb (Reaper)" /></td>
<td><img src="{{ stimpack_reaper_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Reaper)' in acquired_items }}" title="{{ stimpack_reaper_name }}" /></td>
<td><img src="{{ icons['Laser Targeting System (Reaper)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Reaper)' in acquired_items }}" title="Laser Targeting System (Reaper)" /></td>
<td><img src="{{ icons['Advanced Cloaking Field (Reaper)'] }}" class="{{ 'acquired' if 'Advanced Cloaking Field (Reaper)' in acquired_items }}" title="Advanced Cloaking Field (Reaper)" /></td>
<td><img src="{{ icons['Spider Mines (Reaper)'] }}" class="{{ 'acquired' if 'Spider Mines (Reaper)' in acquired_items }}" title="Spider Mines (Reaper)" /></td>
<td></td>
<td><img src="{{ icons['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></td>
<td><img src="{{ icons['Tri-Lithium Power Cell (Diamondback)'] }}" class="{{ 'acquired' if 'Tri-Lithium Power Cell (Diamondback)' in acquired_items }}" title="Tri-Lithium Power Cell (Diamondback)" /></td>
<td><img src="{{ icons['Shaped Hull (Diamondback)'] }}" class="{{ 'acquired' if 'Shaped Hull (Diamondback)' in acquired_items }}" title="Shaped Hull (Diamondback)" /></td>
<td><img src="{{ icons['Hyperfluxor (Diamondback)'] }}" class="{{ 'acquired' if 'Hyperfluxor (Diamondback)' in acquired_items }}" title="Hyperfluxor (Diamondback)" /></td>
<td><img src="{{ icons['Burst Capacitors (Diamondback)'] }}" class="{{ 'acquired' if 'Burst Capacitors (Diamondback)' in acquired_items }}" title="Burst Capacitors (Diamondback)" /></td>
<td><img src="{{ icons['Optimized Logistics (Diamondback)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Diamondback)' in acquired_items }}" title="Optimized Logistics (Diamondback)" /></td>
</tr>
<tr>
<td></td>
<td><img src="{{ icons['Combat Drugs (Reaper)'] }}" class="{{ 'acquired' if 'Combat Drugs (Reaper)' in acquired_items }}" title="Combat Drugs (Reaper)" /></td>
<td colspan="6"></td>
<td><img src="{{ icons['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
<td><img src="{{ icons['Maelstrom Rounds (Siege Tank)'] }}" class="{{ 'acquired' if 'Maelstrom Rounds (Siege Tank)' in acquired_items }}" title="Maelstrom Rounds (Siege Tank)" /></td>
<td><img src="{{ icons['Shaped Blast (Siege Tank)'] }}" class="{{ 'acquired' if 'Shaped Blast (Siege Tank)' in acquired_items }}" title="Shaped Blast (Siege Tank)" /></td>
<td><img src="{{ icons['Jump Jets (Siege Tank)'] }}" class="{{ 'acquired' if 'Jump Jets (Siege Tank)' in acquired_items }}" title="Jump Jets (Siege Tank)" /></td>
<td><img src="{{ icons['Spider Mines (Siege Tank)'] }}" class="{{ 'acquired' if 'Spider Mines (Siege Tank)' in acquired_items }}" title="Spider Mines (Siege Tank)" /></td>
<td><img src="{{ icons['Smart Servos (Siege Tank)'] }}" class="{{ 'acquired' if 'Smart Servos (Siege Tank)' in acquired_items }}" title="Smart Servos (Siege Tank)" /></td>
<td><img src="{{ icons['Graduating Range (Siege Tank)'] }}" class="{{ 'acquired' if 'Graduating Range (Siege Tank)' in acquired_items }}" title="Graduating Range (Siege Tank)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
<td><img src="{{ icons['Ocular Implants (Ghost)'] }}" class="{{ 'acquired' if 'Ocular Implants (Ghost)' in acquired_items }}" title="Ocular Implants (Ghost)" /></td>
<td><img src="{{ icons['Crius Suit (Ghost)'] }}" class="{{ 'acquired' if 'Crius Suit (Ghost)' in acquired_items }}" title="Crius Suit (Ghost)" /></td>
<td><img src="{{ icons['EMP Rounds (Ghost)'] }}" class="{{ 'acquired' if 'EMP Rounds (Ghost)' in acquired_items }}" title="EMP Rounds (Ghost)" /></td>
<td><img src="{{ icons['Lockdown (Ghost)'] }}" class="{{ 'acquired' if 'Lockdown (Ghost)' in acquired_items }}" title="Lockdown (Ghost)" /></td>
<td colspan="3"></td>
<td></td>
<td><img src="{{ icons['Laser Targeting System (Siege Tank)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Siege Tank)' in acquired_items }}" title="Laser Targeting System (Siege Tank)" /></td>
<td><img src="{{ icons['Advanced Siege Tech (Siege Tank)'] }}" class="{{ 'acquired' if 'Advanced Siege Tech (Siege Tank)' in acquired_items }}" title="Advanced Siege Tech (Siege Tank)" /></td>
<td><img src="{{ icons['Internal Tech Module (Siege Tank)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Siege Tank)' in acquired_items }}" title="Internal Tech Module (Siege Tank)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Spectre' in acquired_items }}" title="Spectre" /></td>
<td><img src="{{ icons['Psionic Lash (Spectre)'] }}" class="{{ 'acquired' if 'Psionic Lash (Spectre)' in acquired_items }}" title="Psionic Lash (Spectre)" /></td>
<td><img src="{{ icons['Nyx-Class Cloaking Module (Spectre)'] }}" class="{{ 'acquired' if 'Nyx-Class Cloaking Module (Spectre)' in acquired_items }}" title="Nyx-Class Cloaking Module (Spectre)" /></td>
<td><img src="{{ icons['Impaler Rounds (Spectre)'] }}" class="{{ 'acquired' if 'Impaler Rounds (Spectre)' in acquired_items }}" title="Impaler Rounds (Spectre)" /></td>
<td colspan="4"></td>
<td><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Thor' in acquired_items }}" title="Thor" /></td>
<td><img src="{{ icons['330mm Barrage Cannon (Thor)'] }}" class="{{ 'acquired' if '330mm Barrage Cannon (Thor)' in acquired_items }}" title="330mm Barrage Cannon (Thor)" /></td>
<td><img src="{{ icons['Immortality Protocol (Thor)'] }}" class="{{ 'acquired' if 'Immortality Protocol (Thor)' in acquired_items }}" title="Immortality Protocol (Thor)" /></td>
<td><img src="{{ high_impact_payload_thor_url }}" class="{{ 'acquired' if 'Progressive High Impact Payload (Thor)' in acquired_items }}" title="{{ high_impact_payload_thor_name }}" /></td>
</tr>
<tr>
<td colspan="8"></td>
<td><img src="{{ icons['Predator'] }}" class="{{ 'acquired' if 'Predator' in acquired_items }}" title="Predator" /></td>
<td><img src="{{ icons['Optimized Logistics (Predator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Predator)' in acquired_items }}" title="Optimized Logistics (Predator)" /></td>
</tr>
<tr>
<td colspan="8"></td>
<td><img src="{{ icons['Widow Mine'] }}" class="{{ 'acquired' if 'Widow Mine' in acquired_items }}" title="Widow Mine" /></td>
<td><img src="{{ icons['Drilling Claws (Widow Mine)'] }}" class="{{ 'acquired' if 'Drilling Claws (Widow Mine)' in acquired_items }}" title="Drilling Claws (Widow Mine)" /></td>
<td><img src="{{ icons['Concealment (Widow Mine)'] }}" class="{{ 'acquired' if 'Concealment (Widow Mine)' in acquired_items }}" title="Concealment (Widow Mine)" /></td>
<td><img src="{{ icons['Black Market Launchers (Widow Mine)'] }}" class="{{ 'acquired' if 'Black Market Launchers (Widow Mine)' in acquired_items }}" title="Black Market Launchers (Widow Mine)" /></td>
<td><img src="{{ icons['Executioner Missiles (Widow Mine)'] }}" class="{{ 'acquired' if 'Executioner Missiles (Widow Mine)' in acquired_items }}" title="Executioner Missiles (Widow Mine)" /></td>
</tr>
<tr>
<td colspan="8"></td>
<td><img src="{{ icons['Cyclone'] }}" class="{{ 'acquired' if 'Cyclone' in acquired_items }}" title="Cyclone" /></td>
<td><img src="{{ icons['Mag-Field Accelerators (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Accelerators (Cyclone)' in acquired_items }}" title="Mag-Field Accelerators (Cyclone)" /></td>
<td><img src="{{ icons['Mag-Field Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Launchers (Cyclone)' in acquired_items }}" title="Mag-Field Launchers (Cyclone)" /></td>
<td><img src="{{ icons['Targeting Optics (Cyclone)'] }}" class="{{ 'acquired' if 'Targeting Optics (Cyclone)' in acquired_items }}" title="Targeting Optics (Cyclone)" /></td>
<td><img src="{{ icons['Rapid Fire Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Rapid Fire Launchers (Cyclone)' in acquired_items }}" title="Rapid Fire Launchers (Cyclone)" /></td>
</tr>
<tr>
<td colspan="15" class="title">
Starships
</td>
</tr>
<tr>
<td><img src="{{ icons['Medivac'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="Medivac" /></td>
<td><img src="{{ icons['Rapid Deployment Tube (Medivac)'] }}" class="{{ 'acquired' if 'Rapid Deployment Tube (Medivac)' in acquired_items }}" title="Rapid Deployment Tube (Medivac)" /></td>
<td><img src="{{ icons['Advanced Healing AI (Medivac)'] }}" class="{{ 'acquired' if 'Advanced Healing AI (Medivac)' in acquired_items }}" title="Advanced Healing AI (Medivac)" /></td>
<td><img src="{{ icons['Expanded Hull (Medivac)'] }}" class="{{ 'acquired' if 'Expanded Hull (Medivac)' in acquired_items }}" title="Expanded Hull (Medivac)" /></td>
<td><img src="{{ icons['Afterburners (Medivac)'] }}" class="{{ 'acquired' if 'Afterburners (Medivac)' in acquired_items }}" title="Afterburners (Medivac)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Raven'] }}" class="{{ 'acquired' if 'Raven' in acquired_items }}" title="Raven" /></td>
<td><img src="{{ icons['Bio Mechanical Repair Drone (Raven)'] }}" class="{{ 'acquired' if 'Bio Mechanical Repair Drone (Raven)' in acquired_items }}" title="Bio Mechanical Repair Drone (Raven)" /></td>
<td><img src="{{ icons['Spider Mines (Raven)'] }}" class="{{ 'acquired' if 'Spider Mines (Raven)' in acquired_items }}" title="Spider Mines (Raven)" /></td>
<td><img src="{{ icons['Railgun Turret (Raven)'] }}" class="{{ 'acquired' if 'Railgun Turret (Raven)' in acquired_items }}" title="Railgun Turret (Raven)" /></td>
<td><img src="{{ icons['Hunter-Seeker Weapon (Raven)'] }}" class="{{ 'acquired' if 'Hunter-Seeker Weapon (Raven)' in acquired_items }}" title="Hunter-Seeker Weapon (Raven)" /></td>
<td><img src="{{ icons['Interference Matrix (Raven)'] }}" class="{{ 'acquired' if 'Interference Matrix (Raven)' in acquired_items }}" title="Interference Matrix (Raven)" /></td>
<td><img src="{{ icons['Anti-Armor Missile (Raven)'] }}" class="{{ 'acquired' if 'Anti-Armor Missile (Raven)' in acquired_items }}" title="Anti-Armor Missile (Raven)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Wraith'] }}" class="{{ 'acquired' if 'Wraith' in acquired_items }}" title="Wraith" /></td>
<td><img src="{{ icons['Tomahawk Power Cells (Wraith)'] }}" class="{{ 'acquired' if 'Tomahawk Power Cells (Wraith)' in acquired_items }}" title="Tomahawk Power Cells (Wraith)" /></td>
<td><img src="{{ icons['Displacement Field (Wraith)'] }}" class="{{ 'acquired' if 'Displacement Field (Wraith)' in acquired_items }}" title="Displacement Field (Wraith)" /></td>
<td><img src="{{ icons['Advanced Laser Technology (Wraith)'] }}" class="{{ 'acquired' if 'Advanced Laser Technology (Wraith)' in acquired_items }}" title="Advanced Laser Technology (Wraith)" /></td>
<td colspan="4"></td>
<td></td>
<td><img src="{{ icons['Internal Tech Module (Raven)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Raven)' in acquired_items }}" title="Internal Tech Module (Raven)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Viking'] }}" class="{{ 'acquired' if 'Viking' in acquired_items }}" title="Viking" /></td>
<td><img src="{{ icons['Ripwave Missiles (Viking)'] }}" class="{{ 'acquired' if 'Ripwave Missiles (Viking)' in acquired_items }}" title="Ripwave Missiles (Viking)" /></td>
<td><img src="{{ icons['Phobos-Class Weapons System (Viking)'] }}" class="{{ 'acquired' if 'Phobos-Class Weapons System (Viking)' in acquired_items }}" title="Phobos-Class Weapons System (Viking)" /></td>
<td><img src="{{ icons['Smart Servos (Viking)'] }}" class="{{ 'acquired' if 'Smart Servos (Viking)' in acquired_items }}" title="Smart Servos (Viking)" /></td>
<td><img src="{{ icons['Magrail Munitions (Viking)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Viking)' in acquired_items }}" title="Magrail Munitions (Viking)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Science Vessel'] }}" class="{{ 'acquired' if 'Science Vessel' in acquired_items }}" title="Science Vessel" /></td>
<td><img src="{{ icons['EMP Shockwave (Science Vessel)'] }}" class="{{ 'acquired' if 'EMP Shockwave (Science Vessel)' in acquired_items }}" title="EMP Shockwave (Science Vessel)" /></td>
<td><img src="{{ icons['Defensive Matrix (Science Vessel)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Science Vessel)' in acquired_items }}" title="Defensive Matrix (Science Vessel)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Banshee'] }}" class="{{ 'acquired' if 'Banshee' in acquired_items }}" title="Banshee" /></td>
<td><img src="{{ crossspectrum_dampeners_banshee_url }}" class="{{ 'acquired' if 'Progressive Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="{{ crossspectrum_dampeners_banshee_name }}" /></td>
<td><img src="{{ icons['Shockwave Missile Battery (Banshee)'] }}" class="{{ 'acquired' if 'Shockwave Missile Battery (Banshee)' in acquired_items }}" title="Shockwave Missile Battery (Banshee)" /></td>
<td><img src="{{ icons['Hyperflight Rotors (Banshee)'] }}" class="{{ 'acquired' if 'Hyperflight Rotors (Banshee)' in acquired_items }}" title="Hyperflight Rotors (Banshee)" /></td>
<td><img src="{{ icons['Laser Targeting System (Banshee)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Banshee)' in acquired_items }}" title="Laser Targeting System (Banshee)" /></td>
<td><img src="{{ icons['Internal Tech Module (Banshee)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Banshee)' in acquired_items }}" title="Internal Tech Module (Banshee)" /></td>
<td colspan="2"></td>
<td><img src="{{ icons['Hercules'] }}" class="{{ 'acquired' if 'Hercules' in acquired_items }}" title="Hercules" /></td>
</tr>
<tr>
<td><img src="{{ icons['Battlecruiser'] }}" class="{{ 'acquired' if 'Battlecruiser' in acquired_items }}" title="Battlecruiser" /></td>
<td><img src="{{ icons['Missile Pods (Battlecruiser)'] }}" class="{{ 'acquired' if 'Missile Pods (Battlecruiser)' in acquired_items }}" title="Missile Pods (Battlecruiser)" /></td>
<td><img src="{{ icons['Defensive Matrix (Battlecruiser)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Battlecruiser)' in acquired_items }}" title="Defensive Matrix (Battlecruiser)" /></td>
<td><img src="{{ icons['Tactical Jump (Battlecruiser)'] }}" class="{{ 'acquired' if 'Tactical Jump (Battlecruiser)' in acquired_items }}" title="Tactical Jump (Battlecruiser)" /></td>
<td><img src="{{ icons['Cloak (Battlecruiser)'] }}" class="{{ 'acquired' if 'Cloak (Battlecruiser)' in acquired_items }}" title="Cloak (Battlecruiser)" /></td>
<td><img src="{{ icons['ATX Laser Battery (Battlecruiser)'] }}" class="{{ 'acquired' if 'ATX Laser Battery (Battlecruiser)' in acquired_items }}" title="ATX Laser Battery (Battlecruiser)" /></td>
<td><img src="{{ icons['Optimized Logistics (Battlecruiser)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Battlecruiser)' in acquired_items }}" title="Optimized Logistics (Battlecruiser)" /></td>
<td></td>
<td><img src="{{ icons['Liberator'] }}" class="{{ 'acquired' if 'Liberator' in acquired_items }}" title="Liberator" /></td>
<td><img src="{{ icons['Advanced Ballistics (Liberator)'] }}" class="{{ 'acquired' if 'Advanced Ballistics (Liberator)' in acquired_items }}" title="Advanced Ballistics (Liberator)" /></td>
<td><img src="{{ icons['Raid Artillery (Liberator)'] }}" class="{{ 'acquired' if 'Raid Artillery (Liberator)' in acquired_items }}" title="Raid Artillery (Liberator)" /></td>
<td><img src="{{ icons['Cloak (Liberator)'] }}" class="{{ 'acquired' if 'Cloak (Liberator)' in acquired_items }}" title="Cloak (Liberator)" /></td>
<td><img src="{{ icons['Laser Targeting System (Liberator)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Liberator)' in acquired_items }}" title="Laser Targeting System (Liberator)" /></td>
<td><img src="{{ icons['Optimized Logistics (Liberator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Liberator)' in acquired_items }}" title="Optimized Logistics (Liberator)" /></td>
</tr>
<tr>
<td></td>
<td><img src="{{ icons['Internal Tech Module (Battlecruiser)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Battlecruiser)' in acquired_items }}" title="Internal Tech Module (Battlecruiser)" /></td>
<td colspan="6"></td>
<td><img src="{{ icons['Valkyrie'] }}" class="{{ 'acquired' if 'Valkyrie' in acquired_items }}" title="Valkyrie" /></td>
<td><img src="{{ icons['Enhanced Cluster Launchers (Valkyrie)'] }}" class="{{ 'acquired' if 'Enhanced Cluster Launchers (Valkyrie)' in acquired_items }}" title="Enhanced Cluster Launchers (Valkyrie)" /></td>
<td><img src="{{ icons['Shaped Hull (Valkyrie)'] }}" class="{{ 'acquired' if 'Shaped Hull (Valkyrie)' in acquired_items }}" title="Shaped Hull (Valkyrie)" /></td>
<td><img src="{{ icons['Burst Lasers (Valkyrie)'] }}" class="{{ 'acquired' if 'Burst Lasers (Valkyrie)' in acquired_items }}" title="Burst Lasers (Valkyrie)" /></td>
<td><img src="{{ icons['Afterburners (Valkyrie)'] }}" class="{{ 'acquired' if 'Afterburners (Valkyrie)' in acquired_items }}" title="Afterburners (Valkyrie)" /></td>
</tr>
<tr>
<td colspan="15" class="title">
Mercenaries
</td>
</tr>
<tr>
<td><img src="{{ icons['War Pigs'] }}" class="{{ 'acquired' if 'War Pigs' in acquired_items }}" title="War Pigs" /></td>
<td><img src="{{ icons['Devil Dogs'] }}" class="{{ 'acquired' if 'Devil Dogs' in acquired_items }}" title="Devil Dogs" /></td>
<td><img src="{{ icons['Hammer Securities'] }}" class="{{ 'acquired' if 'Hammer Securities' in acquired_items }}" title="Hammer Securities" /></td>
<td><img src="{{ icons['Spartan Company'] }}" class="{{ 'acquired' if 'Spartan Company' in acquired_items }}" title="Spartan Company" /></td>
<td><img src="{{ icons['Siege Breakers'] }}" class="{{ 'acquired' if 'Siege Breakers' in acquired_items }}" title="Siege Breakers" /></td>
<td><img src="{{ icons['Hel\'s Angel'] }}" class="{{ 'acquired' if 'Hel\'s Angel' in acquired_items }}" title="Hel's Angel" /></td>
<td><img src="{{ icons['Dusk Wings'] }}" class="{{ 'acquired' if 'Dusk Wings' in acquired_items }}" title="Dusk Wings" /></td>
<td><img src="{{ icons['Jackson\'s Revenge'] }}" class="{{ 'acquired' if 'Jackson\'s Revenge' in acquired_items }}" title="Jackson's Revenge" /></td>
</tr>
<tr>
<td colspan="15" class="title">
General Upgrades
</td>
</tr>
<tr>
<td><img src="{{ icons['Fire-Suppression System (Building)'] }}" class="{{ 'acquired' if 'Fire-Suppression System (Building)' in acquired_items }}" title="Fire-Suppression System (Building)" /></td>
<td><img src="{{ icons['Orbital Strike'] }}" class="{{ 'acquired' if 'Orbital Strike' in acquired_items }}" title="Orbital Strike" /></td>
<td><img src="{{ icons['Cellular Reactor'] }}" class="{{ 'acquired' if 'Cellular Reactor' in acquired_items }}" title="Cellular Reactor" /></td>
<td><img src="{{ regenerative_biosteel_url }}" class="{{ 'acquired' if 'Progressive Regenerative Bio-Steel' in acquired_items }}" title="Progressive Regenerative Bio-Steel{% if regenerative_biosteel_level > 0 %} (Level {{ regenerative_biosteel_level }}){% endif %}" /></td>
</tr>
<tr>
<td colspan="15" class="title">
Protoss Units
</td>
</tr>
<tr>
<td><img src="{{ icons['Zealot'] }}" class="{{ 'acquired' if 'Zealot' in acquired_items }}" title="Zealot" /></td>
<td><img src="{{ icons['Stalker'] }}" class="{{ 'acquired' if 'Stalker' in acquired_items }}" title="Stalker" /></td>
<td><img src="{{ icons['High Templar'] }}" class="{{ 'acquired' if 'High Templar' in acquired_items }}" title="High Templar" /></td>
<td><img src="{{ icons['Dark Templar'] }}" class="{{ 'acquired' if 'Dark Templar' in acquired_items }}" title="Dark Templar" /></td>
<td><img src="{{ icons['Immortal'] }}" class="{{ 'acquired' if 'Immortal' in acquired_items }}" title="Immortal" /></td>
<td><img src="{{ icons['Colossus'] }}" class="{{ 'acquired' if 'Colossus' in acquired_items }}" title="Colossus" /></td>
<td><img src="{{ icons['Phoenix'] }}" class="{{ 'acquired' if 'Phoenix' in acquired_items }}" title="Phoenix" /></td>
<td><img src="{{ icons['Void Ray'] }}" class="{{ 'acquired' if 'Void Ray' in acquired_items }}" title="Void Ray" /></td>
<td><img src="{{ icons['Carrier'] }}" class="{{ 'acquired' if 'Carrier' in acquired_items }}" title="Carrier" /></td>
</tr>
</table>
<table id="location-table">
{% for area in checks_in_area %}
{% if checks_in_area[area] > 0 %}
<tr class="location-category" id="{{area}}-header">
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
</tr>
<tbody class="locations hide" id="{{area}}">
{% for location in location_info[area] %}
<tr>
<td class="location-name">{{ location }}</td>
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endif %}
{% endfor %}
</table>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -134,8 +134,8 @@
# Sonic Adventure 2 Battle
/worlds/sa2b/ @PoryGone @RaspberrySpace
# Starcraft 2 Wings of Liberty
/worlds/sc2wol/ @Ziktofel
# Starcraft 2
/worlds/sc2/ @Ziktofel
# Super Metroid
/worlds/sm/ @lordlou

View File

@ -84,7 +84,6 @@ non_apworlds: set = {
# LogicMixin is broken before 3.10 import revamp
if sys.version_info < (3,10):
non_apworlds.add("Hollow Knight")
non_apworlds.add("Starcraft 2 Wings of Liberty")
def download_SNI():
print("Updating SNI")

View File

@ -23,8 +23,8 @@ class TestBase(unittest.TestCase):
{"Pendants", "Crystals"},
"Ocarina of Time":
{"medallions", "stones", "rewards", "logic_bottles"},
"Starcraft 2 Wings of Liberty":
{"Missions"},
"Starcraft 2":
{"Missions", "WoL Missions"},
}
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game_name, game_name=game_name):

View File

@ -12,6 +12,8 @@ from .position import Point2
from .unit import Unit
from .units import Units
from worlds._sc2common.bot import logger
if TYPE_CHECKING:
from .game_info import Ramp
@ -310,6 +312,7 @@ class BotAI(BotAIInternal):
:param message:
:param team_only:"""
assert isinstance(message, str), f"{message} is not a string"
logger.debug("Sending message: " + message)
await self.client.chat_send(message, team_only)
def in_map_bounds(self, pos: Union[Point2, tuple, list]) -> bool:

1631
worlds/sc2/Client.py Normal file

File diff suppressed because it is too large Load Diff

304
worlds/sc2/ClientGui.py Normal file
View File

@ -0,0 +1,304 @@
from typing import *
import asyncio
from kvui import GameManager, HoverBehavior, ServerToolTip
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.tabbedpanel import TabbedPanelItem
from kivy.uix.gridlayout import GridLayout
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.scrollview import ScrollView
from kivy.properties import StringProperty
from worlds.sc2.Client import SC2Context, calc_unfinished_missions, parse_unlock
from worlds.sc2.MissionTables import lookup_id_to_mission, lookup_name_to_mission, campaign_race_exceptions, \
SC2Mission, SC2Race, SC2Campaign
from worlds.sc2.Locations import LocationType, lookup_location_id_to_type
from worlds.sc2.Options import LocationInclusion
from worlds.sc2 import SC2World, get_first_mission
class HoverableButton(HoverBehavior, Button):
pass
class MissionButton(HoverableButton):
tooltip_text = StringProperty("Test")
def __init__(self, *args, **kwargs):
super(HoverableButton, self).__init__(*args, **kwargs)
self.layout = FloatLayout()
self.popuplabel = ServerToolTip(text=self.text, markup=True)
self.popuplabel.padding = [5, 2, 5, 2]
self.layout.add_widget(self.popuplabel)
def on_enter(self):
self.popuplabel.text = self.tooltip_text
if self.ctx.current_tooltip:
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
if self.tooltip_text == "":
self.ctx.current_tooltip = None
else:
App.get_running_app().root.add_widget(self.layout)
self.ctx.current_tooltip = self.layout
def on_leave(self):
self.ctx.ui.clear_tooltip()
@property
def ctx(self) -> SC2Context:
return App.get_running_app().ctx
class CampaignScroll(ScrollView):
pass
class MultiCampaignLayout(GridLayout):
pass
class CampaignLayout(GridLayout):
pass
class MissionLayout(GridLayout):
pass
class MissionCategory(GridLayout):
pass
class SC2Manager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("Starcraft2", "Starcraft2"),
]
base_title = "Archipelago Starcraft 2 Client"
campaign_panel: Optional[CampaignLayout] = None
last_checked_locations: Set[int] = set()
mission_id_to_button: Dict[int, MissionButton] = {}
launching: Union[bool, int] = False # if int -> mission ID
refresh_from_launching = True
first_check = True
first_mission = ""
ctx: SC2Context
def __init__(self, ctx) -> None:
super().__init__(ctx)
def clear_tooltip(self) -> None:
if self.ctx.current_tooltip:
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
self.ctx.current_tooltip = None
def build(self):
container = super().build()
panel = TabbedPanelItem(text="Starcraft 2 Launcher")
panel.content = CampaignScroll()
self.campaign_panel = MultiCampaignLayout()
panel.content.add_widget(self.campaign_panel)
self.tabs.add_widget(panel)
Clock.schedule_interval(self.build_mission_table, 0.5)
return container
def build_mission_table(self, dt) -> None:
if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
not self.refresh_from_launching)) or self.first_check:
assert self.campaign_panel is not None
self.refresh_from_launching = True
self.campaign_panel.clear_widgets()
if self.ctx.mission_req_table:
self.last_checked_locations = self.ctx.checked_locations.copy()
self.first_check = False
self.first_mission = get_first_mission(self.ctx.mission_req_table)
self.mission_id_to_button = {}
available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
multi_campaign_layout_height = 0
for campaign, missions in sorted(self.ctx.mission_req_table.items(), key=lambda item: item[0].id):
categories: Dict[str, List[str]] = {}
# separate missions into categories
for mission_index in missions:
mission_info = self.ctx.mission_req_table[campaign][mission_index]
if mission_info.category not in categories:
categories[mission_info.category] = []
categories[mission_info.category].append(mission_index)
max_mission_count = max(len(categories[category]) for category in categories)
if max_mission_count == 1:
campaign_layout_height = 115
else:
campaign_layout_height = (max_mission_count + 2) * 50
multi_campaign_layout_height += campaign_layout_height
campaign_layout = CampaignLayout(size_hint_y=None, height=campaign_layout_height)
if campaign != SC2Campaign.GLOBAL:
campaign_layout.add_widget(
Label(text=campaign.campaign_name, size_hint_y=None, height=25, outline_width=1)
)
mission_layout = MissionLayout()
for category in categories:
category_name_height = 0
category_spacing = 3
if category.startswith('_'):
category_display_name = ''
else:
category_display_name = category
category_name_height += 25
category_spacing = 10
category_panel = MissionCategory(padding=[category_spacing,6,category_spacing,6])
category_panel.add_widget(
Label(text=category_display_name, size_hint_y=None, height=category_name_height, outline_width=1))
for mission in categories[category]:
text: str = mission
tooltip: str = ""
mission_obj: SC2Mission = lookup_name_to_mission[mission]
mission_id: int = mission_obj.id
mission_data = self.ctx.mission_req_table[campaign][mission]
remaining_locations, plando_locations, remaining_count = self.sort_unfinished_locations(mission)
# Map has uncollected locations
if mission in unfinished_missions:
if self.any_valuable_locations(remaining_locations):
text = f"[color=6495ED]{text}[/color]"
else:
text = f"[color=A0BEF4]{text}[/color]"
elif mission in available_missions:
text = f"[color=FFFFFF]{text}[/color]"
# Map requirements not met
else:
text = f"[color=a9a9a9]{text}[/color]"
tooltip = f"Requires: "
if mission_data.required_world:
tooltip += ", ".join(list(self.ctx.mission_req_table[parse_unlock(req_mission).campaign])[parse_unlock(req_mission).connect_to - 1] for
req_mission in
mission_data.required_world)
if mission_data.number:
tooltip += " and "
if mission_data.number:
tooltip += f"{self.ctx.mission_req_table[campaign][mission].number} missions completed"
if mission_id == self.ctx.final_mission:
if mission in available_missions:
text = f"[color=FFBC95]{mission}[/color]"
else:
text = f"[color=D0C0BE]{mission}[/color]"
if tooltip:
tooltip += "\n"
tooltip += "Final Mission"
if remaining_count > 0:
if tooltip:
tooltip += "\n\n"
tooltip += f"-- Uncollected locations --"
for loctype in LocationType:
if len(remaining_locations[loctype]) > 0:
if loctype == LocationType.VICTORY:
tooltip += f"\n- {remaining_locations[loctype][0]}"
else:
tooltip += f"\n{self.get_location_type_title(loctype)}:\n- "
tooltip += "\n- ".join(remaining_locations[loctype])
if len(plando_locations) > 0:
tooltip += f"\nPlando:\n- "
tooltip += "\n- ".join(plando_locations)
MISSION_BUTTON_HEIGHT = 50
for pad in range(mission_data.ui_vertical_padding):
column_spacer = Label(text='', size_hint_y=None, height=MISSION_BUTTON_HEIGHT)
category_panel.add_widget(column_spacer)
mission_button = MissionButton(text=text, size_hint_y=None, height=MISSION_BUTTON_HEIGHT)
mission_race = mission_obj.race
if mission_race == SC2Race.ANY:
mission_race = mission_obj.campaign.race
race = campaign_race_exceptions.get(mission_obj, mission_race)
racial_colors = {
SC2Race.TERRAN: (0.24, 0.84, 0.68),
SC2Race.ZERG: (1, 0.65, 0.37),
SC2Race.PROTOSS: (0.55, 0.7, 1)
}
if race in racial_colors:
mission_button.background_color = racial_colors[race]
mission_button.tooltip_text = tooltip
mission_button.bind(on_press=self.mission_callback)
self.mission_id_to_button[mission_id] = mission_button
category_panel.add_widget(mission_button)
category_panel.add_widget(Label(text=""))
mission_layout.add_widget(category_panel)
campaign_layout.add_widget(mission_layout)
self.campaign_panel.add_widget(campaign_layout)
self.campaign_panel.height = multi_campaign_layout_height
elif self.launching:
assert self.campaign_panel is not None
self.refresh_from_launching = False
self.campaign_panel.clear_widgets()
self.campaign_panel.add_widget(Label(text="Launching Mission: " +
lookup_id_to_mission[self.launching].mission_name))
if self.ctx.ui:
self.ctx.ui.clear_tooltip()
def mission_callback(self, button: MissionButton) -> None:
if not self.launching:
mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button)
if self.ctx.play_mission(mission_id):
self.launching = mission_id
Clock.schedule_once(self.finish_launching, 10)
def finish_launching(self, dt):
self.launching = False
def sort_unfinished_locations(self, mission_name: str) -> Tuple[Dict[LocationType, List[str]], List[str], int]:
locations: Dict[LocationType, List[str]] = {loctype: [] for loctype in LocationType}
count = 0
for loc in self.ctx.locations_for_mission(mission_name):
if loc in self.ctx.missing_locations:
count += 1
locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names[loc])
plando_locations = []
for plando_loc in self.ctx.plando_locations:
for loctype in LocationType:
if plando_loc in locations[loctype]:
locations[loctype].remove(plando_loc)
plando_locations.append(plando_loc)
return locations, plando_locations, count
def any_valuable_locations(self, locations: Dict[LocationType, List[str]]) -> bool:
for loctype in LocationType:
if len(locations[loctype]) > 0 and self.ctx.location_inclusions[loctype] == LocationInclusion.option_enabled:
return True
return False
def get_location_type_title(self, location_type: LocationType) -> str:
title = location_type.name.title().replace("_", " ")
if self.ctx.location_inclusions[location_type] == LocationInclusion.option_disabled:
title += " (Nothing)"
elif self.ctx.location_inclusions[location_type] == LocationInclusion.option_resources:
title += " (Resources)"
else:
title += ""
return title
def start_gui(context: SC2Context):
context.ui = SC2Manager(context)
context.ui_task = asyncio.create_task(context.ui.async_run(), name="UI")
import pkgutil
data = pkgutil.get_data(SC2World.__module__, "Starcraft2.kv").decode()
Builder.load_string(data)

100
worlds/sc2/ItemGroups.py Normal file
View File

@ -0,0 +1,100 @@
import typing
from . import Items, ItemNames
from .MissionTables import campaign_mission_table, SC2Campaign, SC2Mission
"""
Item name groups, given to Archipelago and used in YAMLs and /received filtering.
For non-developers the following will be useful:
* Items with a bracket get groups named after the unbracketed part
* eg. "Advanced Healing AI (Medivac)" is accessible as "Advanced Healing AI"
* The exception to this are item names that would be ambiguous (eg. "Resource Efficiency")
* Item flaggroups get unique groups as well as combined groups for numbered flaggroups
* eg. "Unit" contains all units, "Armory" contains "Armory 1" through "Armory 6"
* The best place to look these up is at the bottom of Items.py
* Items that have a parent are grouped together
* eg. "Zergling Items" contains all items that have "Zergling" as a parent
* These groups do NOT contain the parent item
* This currently does not include items with multiple potential parents, like some LotV unit upgrades
* All items are grouped by their race ("Terran", "Protoss", "Zerg", "Any")
* Hand-crafted item groups can be found at the bottom of this file
"""
item_name_groups: typing.Dict[str, typing.List[str]] = {}
# Groups for use in world logic
item_name_groups["Missions"] = ["Beat " + mission.mission_name for mission in SC2Mission]
item_name_groups["WoL Missions"] = ["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.WOL]] + \
["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.PROPHECY]]
# These item name groups should not show up in documentation
unlisted_item_name_groups = {
"Missions", "WoL Missions"
}
# Some item names only differ in bracketed parts
# These items are ambiguous for short-hand name groups
bracketless_duplicates: typing.Set[str]
# This is a list of names in ItemNames with bracketed parts removed, for internal use
_shortened_names = [(name[:name.find(' (')] if '(' in name else name)
for name in [ItemNames.__dict__[name] for name in ItemNames.__dir__() if not name.startswith('_')]]
# Remove the first instance of every short-name from the full item list
bracketless_duplicates = set(_shortened_names)
for name in bracketless_duplicates:
_shortened_names.remove(name)
# The remaining short-names are the duplicates
bracketless_duplicates = set(_shortened_names)
del _shortened_names
# All items get sorted into their data type
for item, data in Items.get_full_item_list().items():
# Items get assigned to their flaggroup's type
item_name_groups.setdefault(data.type, []).append(item)
# Numbered flaggroups get sorted into an unnumbered group
# Currently supports numbers of one or two digits
if data.type[-2:].strip().isnumeric:
type_group = data.type[:-2].strip()
item_name_groups.setdefault(type_group, []).append(item)
# Flaggroups with numbers are unlisted
unlisted_item_name_groups.add(data.type)
# Items with a bracket get a short-hand name group for ease of use in YAMLs
if '(' in item:
short_name = item[:item.find(' (')]
# Ambiguous short-names are dropped
if short_name not in bracketless_duplicates:
item_name_groups[short_name] = [item]
# Short-name groups are unlisted
unlisted_item_name_groups.add(short_name)
# Items with a parent get assigned to their parent's group
if data.parent_item:
# The parent groups need a special name, otherwise they are ambiguous with the parent
parent_group = f"{data.parent_item} Items"
item_name_groups.setdefault(parent_group, []).append(item)
# Parent groups are unlisted
unlisted_item_name_groups.add(parent_group)
# All items get assigned to their race's group
race_group = data.race.name.capitalize()
item_name_groups.setdefault(race_group, []).append(item)
# Hand-made groups
item_name_groups["Aiur"] = [
ItemNames.ZEALOT, ItemNames.DRAGOON, ItemNames.SENTRY, ItemNames.AVENGER, ItemNames.HIGH_TEMPLAR,
ItemNames.IMMORTAL, ItemNames.REAVER,
ItemNames.PHOENIX, ItemNames.SCOUT, ItemNames.ARBITER, ItemNames.CARRIER,
]
item_name_groups["Nerazim"] = [
ItemNames.CENTURION, ItemNames.STALKER, ItemNames.DARK_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.DARK_ARCHON,
ItemNames.ANNIHILATOR,
ItemNames.CORSAIR, ItemNames.ORACLE, ItemNames.VOID_RAY,
]
item_name_groups["Tal'Darim"] = [
ItemNames.SUPPLICANT, ItemNames.SLAYER, ItemNames.HAVOC, ItemNames.BLOOD_HUNTER, ItemNames.ASCENDANT,
ItemNames.VANGUARD, ItemNames.WRATHWALKER,
ItemNames.DESTROYER, ItemNames.MOTHERSHIP,
ItemNames.WARP_PRISM_PHASE_BLASTER,
]
item_name_groups["Purifier"] = [
ItemNames.SENTINEL, ItemNames.ADEPT, ItemNames.INSTIGATOR, ItemNames.ENERGIZER,
ItemNames.COLOSSUS, ItemNames.DISRUPTOR,
ItemNames.MIRAGE, ItemNames.TEMPEST,
]

661
worlds/sc2/ItemNames.py Normal file
View File

@ -0,0 +1,661 @@
"""
A complete collection of Starcraft 2 item names as strings.
Users of this data may make some assumptions about the structure of a name:
* The upgrade for a unit will end with the unit's name in parentheses
* Weapon / armor upgrades may be grouped by a common prefix specified within this file
"""
# Terran Units
MARINE = "Marine"
MEDIC = "Medic"
FIREBAT = "Firebat"
MARAUDER = "Marauder"
REAPER = "Reaper"
HELLION = "Hellion"
VULTURE = "Vulture"
GOLIATH = "Goliath"
DIAMONDBACK = "Diamondback"
SIEGE_TANK = "Siege Tank"
MEDIVAC = "Medivac"
WRAITH = "Wraith"
VIKING = "Viking"
BANSHEE = "Banshee"
BATTLECRUISER = "Battlecruiser"
GHOST = "Ghost"
SPECTRE = "Spectre"
THOR = "Thor"
RAVEN = "Raven"
SCIENCE_VESSEL = "Science Vessel"
PREDATOR = "Predator"
HERCULES = "Hercules"
# Extended units
LIBERATOR = "Liberator"
VALKYRIE = "Valkyrie"
WIDOW_MINE = "Widow Mine"
CYCLONE = "Cyclone"
HERC = "HERC"
WARHOUND = "Warhound"
# Terran Buildings
BUNKER = "Bunker"
MISSILE_TURRET = "Missile Turret"
SENSOR_TOWER = "Sensor Tower"
PLANETARY_FORTRESS = "Planetary Fortress"
PERDITION_TURRET = "Perdition Turret"
HIVE_MIND_EMULATOR = "Hive Mind Emulator"
PSI_DISRUPTER = "Psi Disrupter"
# Terran Weapon / Armor Upgrades
TERRAN_UPGRADE_PREFIX = "Progressive Terran"
TERRAN_INFANTRY_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Infantry"
TERRAN_VEHICLE_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Vehicle"
TERRAN_SHIP_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Ship"
PROGRESSIVE_TERRAN_INFANTRY_WEAPON = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_TERRAN_INFANTRY_ARMOR = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Armor"
PROGRESSIVE_TERRAN_VEHICLE_WEAPON = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_TERRAN_VEHICLE_ARMOR = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Armor"
PROGRESSIVE_TERRAN_SHIP_WEAPON = f"{TERRAN_SHIP_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_TERRAN_SHIP_ARMOR = f"{TERRAN_SHIP_UPGRADE_PREFIX} Armor"
PROGRESSIVE_TERRAN_WEAPON_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Weapon Upgrade"
PROGRESSIVE_TERRAN_ARMOR_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Armor Upgrade"
PROGRESSIVE_TERRAN_INFANTRY_UPGRADE = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_TERRAN_VEHICLE_UPGRADE = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_TERRAN_SHIP_UPGRADE = f"{TERRAN_SHIP_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Weapon/Armor Upgrade"
# Mercenaries
WAR_PIGS = "War Pigs"
DEVIL_DOGS = "Devil Dogs"
HAMMER_SECURITIES = "Hammer Securities"
SPARTAN_COMPANY = "Spartan Company"
SIEGE_BREAKERS = "Siege Breakers"
HELS_ANGELS = "Hel's Angels"
DUSK_WINGS = "Dusk Wings"
JACKSONS_REVENGE = "Jackson's Revenge"
SKIBIS_ANGELS = "Skibi's Angels"
DEATH_HEADS = "Death Heads"
WINGED_NIGHTMARES = "Winged Nightmares"
MIDNIGHT_RIDERS = "Midnight Riders"
BRYNHILDS = "Brynhilds"
JOTUN = "Jotun"
# Lab / Global
ULTRA_CAPACITORS = "Ultra-Capacitors"
VANADIUM_PLATING = "Vanadium Plating"
ORBITAL_DEPOTS = "Orbital Depots"
MICRO_FILTERING = "Micro-Filtering"
AUTOMATED_REFINERY = "Automated Refinery"
COMMAND_CENTER_REACTOR = "Command Center Reactor"
TECH_REACTOR = "Tech Reactor"
ORBITAL_STRIKE = "Orbital Strike"
CELLULAR_REACTOR = "Cellular Reactor"
PROGRESSIVE_REGENERATIVE_BIO_STEEL = "Progressive Regenerative Bio-Steel"
PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM = "Progressive Fire-Suppression System"
PROGRESSIVE_ORBITAL_COMMAND = "Progressive Orbital Command"
STRUCTURE_ARMOR = "Structure Armor"
HI_SEC_AUTO_TRACKING = "Hi-Sec Auto Tracking"
ADVANCED_OPTICS = "Advanced Optics"
ROGUE_FORCES = "Rogue Forces"
# Terran Unit Upgrades
BANSHEE_HYPERFLIGHT_ROTORS = "Hyperflight Rotors (Banshee)"
BANSHEE_INTERNAL_TECH_MODULE = "Internal Tech Module (Banshee)"
BANSHEE_LASER_TARGETING_SYSTEM = "Laser Targeting System (Banshee)"
BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS = "Progressive Cross-Spectrum Dampeners (Banshee)"
BANSHEE_SHOCKWAVE_MISSILE_BATTERY = "Shockwave Missile Battery (Banshee)"
BANSHEE_SHAPED_HULL = "Shaped Hull (Banshee)"
BANSHEE_ADVANCED_TARGETING_OPTICS = "Advanced Targeting Optics (Banshee)"
BANSHEE_DISTORTION_BLASTERS = "Distortion Blasters (Banshee)"
BANSHEE_ROCKET_BARRAGE = "Rocket Barrage (Banshee)"
BATTLECRUISER_ATX_LASER_BATTERY = "ATX Laser Battery (Battlecruiser)"
BATTLECRUISER_CLOAK = "Cloak (Battlecruiser)"
BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX = "Progressive Defensive Matrix (Battlecruiser)"
BATTLECRUISER_INTERNAL_TECH_MODULE = "Internal Tech Module (Battlecruiser)"
BATTLECRUISER_PROGRESSIVE_MISSILE_PODS = "Progressive Missile Pods (Battlecruiser)"
BATTLECRUISER_OPTIMIZED_LOGISTICS = "Optimized Logistics (Battlecruiser)"
BATTLECRUISER_TACTICAL_JUMP = "Tactical Jump (Battlecruiser)"
BATTLECRUISER_BEHEMOTH_PLATING = "Behemoth Plating (Battlecruiser)"
BATTLECRUISER_COVERT_OPS_ENGINES = "Covert Ops Engines (Battlecruiser)"
BUNKER_NEOSTEEL_BUNKER = "Neosteel Bunker (Bunker)"
BUNKER_PROJECTILE_ACCELERATOR = "Projectile Accelerator (Bunker)"
BUNKER_SHRIKE_TURRET = "Shrike Turret (Bunker)"
BUNKER_FORTIFIED_BUNKER = "Fortified Bunker (Bunker)"
CYCLONE_MAG_FIELD_ACCELERATORS = "Mag-Field Accelerators (Cyclone)"
CYCLONE_MAG_FIELD_LAUNCHERS = "Mag-Field Launchers (Cyclone)"
CYCLONE_RAPID_FIRE_LAUNCHERS = "Rapid Fire Launchers (Cyclone)"
CYCLONE_TARGETING_OPTICS = "Targeting Optics (Cyclone)"
CYCLONE_RESOURCE_EFFICIENCY = "Resource Efficiency (Cyclone)"
CYCLONE_INTERNAL_TECH_MODULE = "Internal Tech Module (Cyclone)"
DIAMONDBACK_BURST_CAPACITORS = "Burst Capacitors (Diamondback)"
DIAMONDBACK_HYPERFLUXOR = "Hyperfluxor (Diamondback)"
DIAMONDBACK_RESOURCE_EFFICIENCY = "Resource Efficiency (Diamondback)"
DIAMONDBACK_SHAPED_HULL = "Shaped Hull (Diamondback)"
DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL = "Progressive Tri-Lithium Power Cell (Diamondback)"
DIAMONDBACK_ION_THRUSTERS = "Ion Thrusters (Diamondback)"
FIREBAT_INCINERATOR_GAUNTLETS = "Incinerator Gauntlets (Firebat)"
FIREBAT_JUGGERNAUT_PLATING = "Juggernaut Plating (Firebat)"
FIREBAT_RESOURCE_EFFICIENCY = "Resource Efficiency (Firebat)"
FIREBAT_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Firebat)"
FIREBAT_INFERNAL_PRE_IGNITER = "Infernal Pre-Igniter (Firebat)"
FIREBAT_KINETIC_FOAM = "Kinetic Foam (Firebat)"
FIREBAT_NANO_PROJECTORS = "Nano Projectors (Firebat)"
GHOST_CRIUS_SUIT = "Crius Suit (Ghost)"
GHOST_EMP_ROUNDS = "EMP Rounds (Ghost)"
GHOST_LOCKDOWN = "Lockdown (Ghost)"
GHOST_OCULAR_IMPLANTS = "Ocular Implants (Ghost)"
GHOST_RESOURCE_EFFICIENCY = "Resource Efficiency (Ghost)"
GOLIATH_ARES_CLASS_TARGETING_SYSTEM = "Ares-Class Targeting System (Goliath)"
GOLIATH_JUMP_JETS = "Jump Jets (Goliath)"
GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM = "Multi-Lock Weapons System (Goliath)"
GOLIATH_OPTIMIZED_LOGISTICS = "Optimized Logistics (Goliath)"
GOLIATH_SHAPED_HULL = "Shaped Hull (Goliath)"
GOLIATH_RESOURCE_EFFICIENCY = "Resource Efficiency (Goliath)"
GOLIATH_INTERNAL_TECH_MODULE = "Internal Tech Module (Goliath)"
HELLION_HELLBAT_ASPECT = "Hellbat Aspect (Hellion)"
HELLION_JUMP_JETS = "Jump Jets (Hellion)"
HELLION_OPTIMIZED_LOGISTICS = "Optimized Logistics (Hellion)"
HELLION_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Hellion)"
HELLION_SMART_SERVOS = "Smart Servos (Hellion)"
HELLION_THERMITE_FILAMENTS = "Thermite Filaments (Hellion)"
HELLION_TWIN_LINKED_FLAMETHROWER = "Twin-Linked Flamethrower (Hellion)"
HELLION_INFERNAL_PLATING = "Infernal Plating (Hellion)"
HERC_JUGGERNAUT_PLATING = "Juggernaut Plating (HERC)"
HERC_KINETIC_FOAM = "Kinetic Foam (HERC)"
HERC_RESOURCE_EFFICIENCY = "Resource Efficiency (HERC)"
HERCULES_INTERNAL_FUSION_MODULE = "Internal Fusion Module (Hercules)"
HERCULES_TACTICAL_JUMP = "Tactical Jump (Hercules)"
LIBERATOR_ADVANCED_BALLISTICS = "Advanced Ballistics (Liberator)"
LIBERATOR_CLOAK = "Cloak (Liberator)"
LIBERATOR_LASER_TARGETING_SYSTEM = "Laser Targeting System (Liberator)"
LIBERATOR_OPTIMIZED_LOGISTICS = "Optimized Logistics (Liberator)"
LIBERATOR_RAID_ARTILLERY = "Raid Artillery (Liberator)"
LIBERATOR_SMART_SERVOS = "Smart Servos (Liberator)"
LIBERATOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Liberator)"
MARAUDER_CONCUSSIVE_SHELLS = "Concussive Shells (Marauder)"
MARAUDER_INTERNAL_TECH_MODULE = "Internal Tech Module (Marauder)"
MARAUDER_KINETIC_FOAM = "Kinetic Foam (Marauder)"
MARAUDER_LASER_TARGETING_SYSTEM = "Laser Targeting System (Marauder)"
MARAUDER_MAGRAIL_MUNITIONS = "Magrail Munitions (Marauder)"
MARAUDER_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Marauder)"
MARAUDER_JUGGERNAUT_PLATING = "Juggernaut Plating (Marauder)"
MARINE_COMBAT_SHIELD = "Combat Shield (Marine)"
MARINE_LASER_TARGETING_SYSTEM = "Laser Targeting System (Marine)"
MARINE_MAGRAIL_MUNITIONS = "Magrail Munitions (Marine)"
MARINE_OPTIMIZED_LOGISTICS = "Optimized Logistics (Marine)"
MARINE_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Marine)"
MEDIC_ADVANCED_MEDIC_FACILITIES = "Advanced Medic Facilities (Medic)"
MEDIC_OPTICAL_FLARE = "Optical Flare (Medic)"
MEDIC_RESOURCE_EFFICIENCY = "Resource Efficiency (Medic)"
MEDIC_RESTORATION = "Restoration (Medic)"
MEDIC_STABILIZER_MEDPACKS = "Stabilizer Medpacks (Medic)"
MEDIC_ADAPTIVE_MEDPACKS = "Adaptive Medpacks (Medic)"
MEDIC_NANO_PROJECTOR = "Nano Projector (Medic)"
MEDIVAC_ADVANCED_HEALING_AI = "Advanced Healing AI (Medivac)"
MEDIVAC_AFTERBURNERS = "Afterburners (Medivac)"
MEDIVAC_EXPANDED_HULL = "Expanded Hull (Medivac)"
MEDIVAC_RAPID_DEPLOYMENT_TUBE = "Rapid Deployment Tube (Medivac)"
MEDIVAC_SCATTER_VEIL = "Scatter Veil (Medivac)"
MEDIVAC_ADVANCED_CLOAKING_FIELD = "Advanced Cloaking Field (Medivac)"
MISSILE_TURRET_HELLSTORM_BATTERIES = "Hellstorm Batteries (Missile Turret)"
MISSILE_TURRET_TITANIUM_HOUSING = "Titanium Housing (Missile Turret)"
PLANETARY_FORTRESS_PROGRESSIVE_AUGMENTED_THRUSTERS = "Progressive Augmented Thrusters (Planetary Fortress)"
PLANETARY_FORTRESS_ADVANCED_TARGETING = "Advanced Targeting (Planetary Fortress)"
PREDATOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Predator)"
PREDATOR_CLOAK = "Cloak (Predator)"
PREDATOR_CHARGE = "Charge (Predator)"
PREDATOR_PREDATOR_S_FURY = "Predator's Fury (Predator)"
RAVEN_ANTI_ARMOR_MISSILE = "Anti-Armor Missile (Raven)"
RAVEN_BIO_MECHANICAL_REPAIR_DRONE = "Bio Mechanical Repair Drone (Raven)"
RAVEN_HUNTER_SEEKER_WEAPON = "Hunter-Seeker Weapon (Raven)"
RAVEN_INTERFERENCE_MATRIX = "Interference Matrix (Raven)"
RAVEN_INTERNAL_TECH_MODULE = "Internal Tech Module (Raven)"
RAVEN_RAILGUN_TURRET = "Railgun Turret (Raven)"
RAVEN_SPIDER_MINES = "Spider Mines (Raven)"
RAVEN_RESOURCE_EFFICIENCY = "Resource Efficiency (Raven)"
RAVEN_DURABLE_MATERIALS = "Durable Materials (Raven)"
REAPER_ADVANCED_CLOAKING_FIELD = "Advanced Cloaking Field (Reaper)"
REAPER_COMBAT_DRUGS = "Combat Drugs (Reaper)"
REAPER_G4_CLUSTERBOMB = "G-4 Clusterbomb (Reaper)"
REAPER_LASER_TARGETING_SYSTEM = "Laser Targeting System (Reaper)"
REAPER_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Reaper)"
REAPER_SPIDER_MINES = "Spider Mines (Reaper)"
REAPER_U238_ROUNDS = "U-238 Rounds (Reaper)"
REAPER_JET_PACK_OVERDRIVE = "Jet Pack Overdrive (Reaper)"
SCIENCE_VESSEL_DEFENSIVE_MATRIX = "Defensive Matrix (Science Vessel)"
SCIENCE_VESSEL_EMP_SHOCKWAVE = "EMP Shockwave (Science Vessel)"
SCIENCE_VESSEL_IMPROVED_NANO_REPAIR = "Improved Nano-Repair (Science Vessel)"
SCIENCE_VESSEL_ADVANCED_AI_SYSTEMS = "Advanced AI Systems (Science Vessel)"
SCV_ADVANCED_CONSTRUCTION = "Advanced Construction (SCV)"
SCV_DUAL_FUSION_WELDERS = "Dual-Fusion Welders (SCV)"
SCV_HOSTILE_ENVIRONMENT_ADAPTATION = "Hostile Environment Adaptation (SCV)"
SIEGE_TANK_ADVANCED_SIEGE_TECH = "Advanced Siege Tech (Siege Tank)"
SIEGE_TANK_GRADUATING_RANGE = "Graduating Range (Siege Tank)"
SIEGE_TANK_INTERNAL_TECH_MODULE = "Internal Tech Module (Siege Tank)"
SIEGE_TANK_JUMP_JETS = "Jump Jets (Siege Tank)"
SIEGE_TANK_LASER_TARGETING_SYSTEM = "Laser Targeting System (Siege Tank)"
SIEGE_TANK_MAELSTROM_ROUNDS = "Maelstrom Rounds (Siege Tank)"
SIEGE_TANK_SHAPED_BLAST = "Shaped Blast (Siege Tank)"
SIEGE_TANK_SMART_SERVOS = "Smart Servos (Siege Tank)"
SIEGE_TANK_SPIDER_MINES = "Spider Mines (Siege Tank)"
SIEGE_TANK_SHAPED_HULL = "Shaped Hull (Siege Tank)"
SIEGE_TANK_RESOURCE_EFFICIENCY = "Resource Efficiency (Siege Tank)"
SPECTRE_IMPALER_ROUNDS = "Impaler Rounds (Spectre)"
SPECTRE_NYX_CLASS_CLOAKING_MODULE = "Nyx-Class Cloaking Module (Spectre)"
SPECTRE_PSIONIC_LASH = "Psionic Lash (Spectre)"
SPECTRE_RESOURCE_EFFICIENCY = "Resource Efficiency (Spectre)"
SPIDER_MINE_CERBERUS_MINE = "Cerberus Mine (Spider Mine)"
SPIDER_MINE_HIGH_EXPLOSIVE_MUNITION = "High Explosive Munition (Spider Mine)"
THOR_330MM_BARRAGE_CANNON = "330mm Barrage Cannon (Thor)"
THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL = "Progressive Immortality Protocol (Thor)"
THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD = "Progressive High Impact Payload (Thor)"
THOR_BUTTON_WITH_A_SKULL_ON_IT = "Button With a Skull on It (Thor)"
THOR_LASER_TARGETING_SYSTEM = "Laser Targeting System (Thor)"
THOR_LARGE_SCALE_FIELD_CONSTRUCTION = "Large Scale Field Construction (Thor)"
VALKYRIE_AFTERBURNERS = "Afterburners (Valkyrie)"
VALKYRIE_FLECHETTE_MISSILES = "Flechette Missiles (Valkyrie)"
VALKYRIE_ENHANCED_CLUSTER_LAUNCHERS = "Enhanced Cluster Launchers (Valkyrie)"
VALKYRIE_SHAPED_HULL = "Shaped Hull (Valkyrie)"
VALKYRIE_LAUNCHING_VECTOR_COMPENSATOR = "Launching Vector Compensator (Valkyrie)"
VALKYRIE_RESOURCE_EFFICIENCY = "Resource Efficiency (Valkyrie)"
VIKING_ANTI_MECHANICAL_MUNITION = "Anti-Mechanical Munition (Viking)"
VIKING_PHOBOS_CLASS_WEAPONS_SYSTEM = "Phobos-Class Weapons System (Viking)"
VIKING_RIPWAVE_MISSILES = "Ripwave Missiles (Viking)"
VIKING_SMART_SERVOS = "Smart Servos (Viking)"
VIKING_SHREDDER_ROUNDS = "Shredder Rounds (Viking)"
VIKING_WILD_MISSILES = "W.I.L.D. Missiles (Viking)"
VULTURE_AUTO_LAUNCHERS = "Auto Launchers (Vulture)"
VULTURE_ION_THRUSTERS = "Ion Thrusters (Vulture)"
VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE = "Progressive Replenishable Magazine (Vulture)"
VULTURE_AUTO_REPAIR = "Auto-Repair (Vulture)"
WARHOUND_RESOURCE_EFFICIENCY = "Resource Efficiency (Warhound)"
WARHOUND_REINFORCED_PLATING = "Reinforced Plating (Warhound)"
WIDOW_MINE_BLACK_MARKET_LAUNCHERS = "Black Market Launchers (Widow Mine)"
WIDOW_MINE_CONCEALMENT = "Concealment (Widow Mine)"
WIDOW_MINE_DRILLING_CLAWS = "Drilling Claws (Widow Mine)"
WIDOW_MINE_EXECUTIONER_MISSILES = "Executioner Missiles (Widow Mine)"
WRAITH_ADVANCED_LASER_TECHNOLOGY = "Advanced Laser Technology (Wraith)"
WRAITH_DISPLACEMENT_FIELD = "Displacement Field (Wraith)"
WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS = "Progressive Tomahawk Power Cells (Wraith)"
WRAITH_TRIGGER_OVERRIDE = "Trigger Override (Wraith)"
WRAITH_INTERNAL_TECH_MODULE = "Internal Tech Module (Wraith)"
WRAITH_RESOURCE_EFFICIENCY = "Resource Efficiency (Wraith)"
# Nova
NOVA_GHOST_VISOR = "Ghost Visor (Nova Equipment)"
NOVA_RANGEFINDER_OCULUS = "Rangefinder Oculus (Nova Equipment)"
NOVA_DOMINATION = "Domination (Nova Ability)"
NOVA_BLINK = "Blink (Nova Ability)"
NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE = "Progressive Stealth Suit Module (Nova Suit Module)"
NOVA_ENERGY_SUIT_MODULE = "Energy Suit Module (Nova Suit Module)"
NOVA_ARMORED_SUIT_MODULE = "Armored Suit Module (Nova Suit Module)"
NOVA_JUMP_SUIT_MODULE = "Jump Suit Module (Nova Suit Module)"
NOVA_C20A_CANISTER_RIFLE = "C20A Canister Rifle (Nova Weapon)"
NOVA_HELLFIRE_SHOTGUN = "Hellfire Shotgun (Nova Weapon)"
NOVA_PLASMA_RIFLE = "Plasma Rifle (Nova Weapon)"
NOVA_MONOMOLECULAR_BLADE = "Monomolecular Blade (Nova Weapon)"
NOVA_BLAZEFIRE_GUNBLADE = "Blazefire Gunblade (Nova Weapon)"
NOVA_STIM_INFUSION = "Stim Infusion (Nova Gadget)"
NOVA_PULSE_GRENADES = "Pulse Grenades (Nova Gadget)"
NOVA_FLASHBANG_GRENADES = "Flashbang Grenades (Nova Gadget)"
NOVA_IONIC_FORCE_FIELD = "Ionic Force Field (Nova Gadget)"
NOVA_HOLO_DECOY = "Holo Decoy (Nova Gadget)"
NOVA_NUKE = "Tac Nuke Strike (Nova Ability)"
# Zerg Units
ZERGLING = "Zergling"
SWARM_QUEEN = "Swarm Queen"
ROACH = "Roach"
HYDRALISK = "Hydralisk"
ABERRATION = "Aberration"
MUTALISK = "Mutalisk"
SWARM_HOST = "Swarm Host"
INFESTOR = "Infestor"
ULTRALISK = "Ultralisk"
CORRUPTOR = "Corruptor"
SCOURGE = "Scourge"
BROOD_QUEEN = "Brood Queen"
DEFILER = "Defiler"
# Zerg Buildings
SPORE_CRAWLER = "Spore Crawler"
SPINE_CRAWLER = "Spine Crawler"
# Zerg Weapon / Armor Upgrades
ZERG_UPGRADE_PREFIX = "Progressive Zerg"
ZERG_FLYER_UPGRADE_PREFIX = f"{ZERG_UPGRADE_PREFIX} Flyer"
PROGRESSIVE_ZERG_MELEE_ATTACK = f"{ZERG_UPGRADE_PREFIX} Melee Attack"
PROGRESSIVE_ZERG_MISSILE_ATTACK = f"{ZERG_UPGRADE_PREFIX} Missile Attack"
PROGRESSIVE_ZERG_GROUND_CARAPACE = f"{ZERG_UPGRADE_PREFIX} Ground Carapace"
PROGRESSIVE_ZERG_FLYER_ATTACK = f"{ZERG_FLYER_UPGRADE_PREFIX} Attack"
PROGRESSIVE_ZERG_FLYER_CARAPACE = f"{ZERG_FLYER_UPGRADE_PREFIX} Carapace"
PROGRESSIVE_ZERG_WEAPON_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Weapon Upgrade"
PROGRESSIVE_ZERG_ARMOR_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Armor Upgrade"
PROGRESSIVE_ZERG_GROUND_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Ground Upgrade"
PROGRESSIVE_ZERG_FLYER_UPGRADE = f"{ZERG_FLYER_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Weapon/Armor Upgrade"
# Zerg Unit Upgrades
ZERGLING_HARDENED_CARAPACE = "Hardened Carapace (Zergling)"
ZERGLING_ADRENAL_OVERLOAD = "Adrenal Overload (Zergling)"
ZERGLING_METABOLIC_BOOST = "Metabolic Boost (Zergling)"
ZERGLING_SHREDDING_CLAWS = "Shredding Claws (Zergling)"
ROACH_HYDRIODIC_BILE = "Hydriodic Bile (Roach)"
ROACH_ADAPTIVE_PLATING = "Adaptive Plating (Roach)"
ROACH_TUNNELING_CLAWS = "Tunneling Claws (Roach)"
ROACH_GLIAL_RECONSTITUTION = "Glial Reconstitution (Roach)"
ROACH_ORGANIC_CARAPACE = "Organic Carapace (Roach)"
HYDRALISK_FRENZY = "Frenzy (Hydralisk)"
HYDRALISK_ANCILLARY_CARAPACE = "Ancillary Carapace (Hydralisk)"
HYDRALISK_GROOVED_SPINES = "Grooved Spines (Hydralisk)"
HYDRALISK_MUSCULAR_AUGMENTS = "Muscular Augments (Hydralisk)"
HYDRALISK_RESOURCE_EFFICIENCY = "Resource Efficiency (Hydralisk)"
BANELING_CORROSIVE_ACID = "Corrosive Acid (Baneling)"
BANELING_RUPTURE = "Rupture (Baneling)"
BANELING_REGENERATIVE_ACID = "Regenerative Acid (Baneling)"
BANELING_CENTRIFUGAL_HOOKS = "Centrifugal Hooks (Baneling)"
BANELING_TUNNELING_JAWS = "Tunneling Jaws (Baneling)"
BANELING_RAPID_METAMORPH = "Rapid Metamorph (Baneling)"
MUTALISK_VICIOUS_GLAIVE = "Vicious Glaive (Mutalisk)"
MUTALISK_RAPID_REGENERATION = "Rapid Regeneration (Mutalisk)"
MUTALISK_SUNDERING_GLAIVE = "Sundering Glaive (Mutalisk)"
MUTALISK_SEVERING_GLAIVE = "Severing Glaive (Mutalisk)"
MUTALISK_AERODYNAMIC_GLAIVE_SHAPE = "Aerodynamic Glaive Shape (Mutalisk)"
SWARM_HOST_BURROW = "Burrow (Swarm Host)"
SWARM_HOST_RAPID_INCUBATION = "Rapid Incubation (Swarm Host)"
SWARM_HOST_PRESSURIZED_GLANDS = "Pressurized Glands (Swarm Host)"
SWARM_HOST_LOCUST_METABOLIC_BOOST = "Locust Metabolic Boost (Swarm Host)"
SWARM_HOST_ENDURING_LOCUSTS = "Enduring Locusts (Swarm Host)"
SWARM_HOST_ORGANIC_CARAPACE = "Organic Carapace (Swarm Host)"
SWARM_HOST_RESOURCE_EFFICIENCY = "Resource Efficiency (Swarm Host)"
ULTRALISK_BURROW_CHARGE = "Burrow Charge (Ultralisk)"
ULTRALISK_TISSUE_ASSIMILATION = "Tissue Assimilation (Ultralisk)"
ULTRALISK_MONARCH_BLADES = "Monarch Blades (Ultralisk)"
ULTRALISK_ANABOLIC_SYNTHESIS = "Anabolic Synthesis (Ultralisk)"
ULTRALISK_CHITINOUS_PLATING = "Chitinous Plating (Ultralisk)"
ULTRALISK_ORGANIC_CARAPACE = "Organic Carapace (Ultralisk)"
ULTRALISK_RESOURCE_EFFICIENCY = "Resource Efficiency (Ultralisk)"
CORRUPTOR_CORRUPTION = "Corruption (Corruptor)"
CORRUPTOR_CAUSTIC_SPRAY = "Caustic Spray (Corruptor)"
SCOURGE_VIRULENT_SPORES = "Virulent Spores (Scourge)"
SCOURGE_RESOURCE_EFFICIENCY = "Resource Efficiency (Scourge)"
SCOURGE_SWARM_SCOURGE = "Swarm Scourge (Scourge)"
DEVOURER_CORROSIVE_SPRAY = "Corrosive Spray (Devourer)"
DEVOURER_GAPING_MAW = "Gaping Maw (Devourer)"
DEVOURER_IMPROVED_OSMOSIS = "Improved Osmosis (Devourer)"
DEVOURER_PRESCIENT_SPORES = "Prescient Spores (Devourer)"
GUARDIAN_PROLONGED_DISPERSION = "Prolonged Dispersion (Guardian)"
GUARDIAN_PRIMAL_ADAPTATION = "Primal Adaptation (Guardian)"
GUARDIAN_SORONAN_ACID = "Soronan Acid (Guardian)"
IMPALER_ADAPTIVE_TALONS = "Adaptive Talons (Impaler)"
IMPALER_SECRETION_GLANDS = "Secretion Glands (Impaler)"
IMPALER_HARDENED_TENTACLE_SPINES = "Hardened Tentacle Spines (Impaler)"
LURKER_SEISMIC_SPINES = "Seismic Spines (Lurker)"
LURKER_ADAPTED_SPINES = "Adapted Spines (Lurker)"
RAVAGER_POTENT_BILE = "Potent Bile (Ravager)"
RAVAGER_BLOATED_BILE_DUCTS = "Bloated Bile Ducts (Ravager)"
RAVAGER_DEEP_TUNNEL = "Deep Tunnel (Ravager)"
VIPER_PARASITIC_BOMB = "Parasitic Bomb (Viper)"
VIPER_PARALYTIC_BARBS = "Paralytic Barbs (Viper)"
VIPER_VIRULENT_MICROBES = "Virulent Microbes (Viper)"
BROOD_LORD_POROUS_CARTILAGE = "Porous Cartilage (Brood Lord)"
BROOD_LORD_EVOLVED_CARAPACE = "Evolved Carapace (Brood Lord)"
BROOD_LORD_SPLITTER_MITOSIS = "Splitter Mitosis (Brood Lord)"
BROOD_LORD_RESOURCE_EFFICIENCY = "Resource Efficiency (Brood Lord)"
INFESTOR_INFESTED_TERRAN = "Infested Terran (Infestor)"
INFESTOR_MICROBIAL_SHROUD = "Microbial Shroud (Infestor)"
SWARM_QUEEN_SPAWN_LARVAE = "Spawn Larvae (Swarm Queen)"
SWARM_QUEEN_DEEP_TUNNEL = "Deep Tunnel (Swarm Queen)"
SWARM_QUEEN_ORGANIC_CARAPACE = "Organic Carapace (Swarm Queen)"
SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION = "Bio-Mechanical Transfusion (Swarm Queen)"
SWARM_QUEEN_RESOURCE_EFFICIENCY = "Resource Efficiency (Swarm Queen)"
SWARM_QUEEN_INCUBATOR_CHAMBER = "Incubator Chamber (Swarm Queen)"
BROOD_QUEEN_FUNGAL_GROWTH = "Fungal Growth (Brood Queen)"
BROOD_QUEEN_ENSNARE = "Ensnare (Brood Queen)"
BROOD_QUEEN_ENHANCED_MITOCHONDRIA = "Enhanced Mitochondria (Brood Queen)"
# Zerg Strains
ZERGLING_RAPTOR_STRAIN = "Raptor Strain (Zergling)"
ZERGLING_SWARMLING_STRAIN = "Swarmling Strain (Zergling)"
ROACH_VILE_STRAIN = "Vile Strain (Roach)"
ROACH_CORPSER_STRAIN = "Corpser Strain (Roach)"
BANELING_SPLITTER_STRAIN = "Splitter Strain (Baneling)"
BANELING_HUNTER_STRAIN = "Hunter Strain (Baneling)"
SWARM_HOST_CARRION_STRAIN = "Carrion Strain (Swarm Host)"
SWARM_HOST_CREEPER_STRAIN = "Creeper Strain (Swarm Host)"
ULTRALISK_NOXIOUS_STRAIN = "Noxious Strain (Ultralisk)"
ULTRALISK_TORRASQUE_STRAIN = "Torrasque Strain (Ultralisk)"
# Morphs
ZERGLING_BANELING_ASPECT = "Baneling Aspect (Zergling)"
HYDRALISK_IMPALER_ASPECT = "Impaler Aspect (Hydralisk)"
HYDRALISK_LURKER_ASPECT = "Lurker Aspect (Hydralisk)"
MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT = "Brood Lord Aspect (Mutalisk/Corruptor)"
MUTALISK_CORRUPTOR_VIPER_ASPECT = "Viper Aspect (Mutalisk/Corruptor)"
MUTALISK_CORRUPTOR_GUARDIAN_ASPECT = "Guardian Aspect (Mutalisk/Corruptor)"
MUTALISK_CORRUPTOR_DEVOURER_ASPECT = "Devourer Aspect (Mutalisk/Corruptor)"
ROACH_RAVAGER_ASPECT = "Ravager Aspect (Roach)"
# Zerg Mercs
INFESTED_MEDICS = "Infested Medics"
INFESTED_SIEGE_TANKS = "Infested Siege Tanks"
INFESTED_BANSHEES = "Infested Banshees"
# Kerrigan Upgrades
KERRIGAN_KINETIC_BLAST = "Kinetic Blast (Kerrigan Tier 1)"
KERRIGAN_HEROIC_FORTITUDE = "Heroic Fortitude (Kerrigan Tier 1)"
KERRIGAN_LEAPING_STRIKE = "Leaping Strike (Kerrigan Tier 1)"
KERRIGAN_CRUSHING_GRIP = "Crushing Grip (Kerrigan Tier 2)"
KERRIGAN_CHAIN_REACTION = "Chain Reaction (Kerrigan Tier 2)"
KERRIGAN_PSIONIC_SHIFT = "Psionic Shift (Kerrigan Tier 2)"
KERRIGAN_WILD_MUTATION = "Wild Mutation (Kerrigan Tier 4)"
KERRIGAN_SPAWN_BANELINGS = "Spawn Banelings (Kerrigan Tier 4)"
KERRIGAN_MEND = "Mend (Kerrigan Tier 4)"
KERRIGAN_INFEST_BROODLINGS = "Infest Broodlings (Kerrigan Tier 6)"
KERRIGAN_FURY = "Fury (Kerrigan Tier 6)"
KERRIGAN_ABILITY_EFFICIENCY = "Ability Efficiency (Kerrigan Tier 6)"
KERRIGAN_APOCALYPSE = "Apocalypse (Kerrigan Tier 7)"
KERRIGAN_SPAWN_LEVIATHAN = "Spawn Leviathan (Kerrigan Tier 7)"
KERRIGAN_DROP_PODS = "Drop-Pods (Kerrigan Tier 7)"
KERRIGAN_PRIMAL_FORM = "Primal Form (Kerrigan)"
# Misc Upgrades
KERRIGAN_ZERGLING_RECONSTITUTION = "Zergling Reconstitution (Kerrigan Tier 3)"
KERRIGAN_IMPROVED_OVERLORDS = "Improved Overlords (Kerrigan Tier 3)"
KERRIGAN_AUTOMATED_EXTRACTORS = "Automated Extractors (Kerrigan Tier 3)"
KERRIGAN_TWIN_DRONES = "Twin Drones (Kerrigan Tier 5)"
KERRIGAN_MALIGNANT_CREEP = "Malignant Creep (Kerrigan Tier 5)"
KERRIGAN_VESPENE_EFFICIENCY = "Vespene Efficiency (Kerrigan Tier 5)"
OVERLORD_VENTRAL_SACS = "Ventral Sacs (Overlord)"
# Kerrigan Levels
KERRIGAN_LEVELS_1 = "1 Kerrigan Level"
KERRIGAN_LEVELS_2 = "2 Kerrigan Levels"
KERRIGAN_LEVELS_3 = "3 Kerrigan Levels"
KERRIGAN_LEVELS_4 = "4 Kerrigan Levels"
KERRIGAN_LEVELS_5 = "5 Kerrigan Levels"
KERRIGAN_LEVELS_6 = "6 Kerrigan Levels"
KERRIGAN_LEVELS_7 = "7 Kerrigan Levels"
KERRIGAN_LEVELS_8 = "8 Kerrigan Levels"
KERRIGAN_LEVELS_9 = "9 Kerrigan Levels"
KERRIGAN_LEVELS_10 = "10 Kerrigan Levels"
KERRIGAN_LEVELS_14 = "14 Kerrigan Levels"
KERRIGAN_LEVELS_35 = "35 Kerrigan Levels"
KERRIGAN_LEVELS_70 = "70 Kerrigan Levels"
# Protoss Units
ZEALOT = "Zealot"
STALKER = "Stalker"
HIGH_TEMPLAR = "High Templar"
DARK_TEMPLAR = "Dark Templar"
IMMORTAL = "Immortal"
COLOSSUS = "Colossus"
PHOENIX = "Phoenix"
VOID_RAY = "Void Ray"
CARRIER = "Carrier"
OBSERVER = "Observer"
CENTURION = "Centurion"
SENTINEL = "Sentinel"
SUPPLICANT = "Supplicant"
INSTIGATOR = "Instigator"
SLAYER = "Slayer"
SENTRY = "Sentry"
ENERGIZER = "Energizer"
HAVOC = "Havoc"
SIGNIFIER = "Signifier"
ASCENDANT = "Ascendant"
AVENGER = "Avenger"
BLOOD_HUNTER = "Blood Hunter"
DRAGOON = "Dragoon"
DARK_ARCHON = "Dark Archon"
ADEPT = "Adept"
WARP_PRISM = "Warp Prism"
ANNIHILATOR = "Annihilator"
VANGUARD = "Vanguard"
WRATHWALKER = "Wrathwalker"
REAVER = "Reaver"
DISRUPTOR = "Disruptor"
MIRAGE = "Mirage"
CORSAIR = "Corsair"
DESTROYER = "Destroyer"
SCOUT = "Scout"
TEMPEST = "Tempest"
MOTHERSHIP = "Mothership"
ARBITER = "Arbiter"
ORACLE = "Oracle"
# Upgrades
PROTOSS_UPGRADE_PREFIX = "Progressive Protoss"
PROTOSS_GROUND_UPGRADE_PREFIX = f"{PROTOSS_UPGRADE_PREFIX} Ground"
PROTOSS_AIR_UPGRADE_PREFIX = f"{PROTOSS_UPGRADE_PREFIX} Air"
PROGRESSIVE_PROTOSS_GROUND_WEAPON = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_PROTOSS_GROUND_ARMOR = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Armor"
PROGRESSIVE_PROTOSS_SHIELDS = f"{PROTOSS_UPGRADE_PREFIX} Shields"
PROGRESSIVE_PROTOSS_AIR_WEAPON = f"{PROTOSS_AIR_UPGRADE_PREFIX} Weapon"
PROGRESSIVE_PROTOSS_AIR_ARMOR = f"{PROTOSS_AIR_UPGRADE_PREFIX} Armor"
PROGRESSIVE_PROTOSS_WEAPON_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Weapon Upgrade"
PROGRESSIVE_PROTOSS_ARMOR_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Armor Upgrade"
PROGRESSIVE_PROTOSS_GROUND_UPGRADE = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_PROTOSS_AIR_UPGRADE = f"{PROTOSS_AIR_UPGRADE_PREFIX} Upgrade"
PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Weapon/Armor Upgrade"
# Buildings
PHOTON_CANNON = "Photon Cannon"
KHAYDARIN_MONOLITH = "Khaydarin Monolith"
SHIELD_BATTERY = "Shield Battery"
# Unit Upgrades
SUPPLICANT_BLOOD_SHIELD = "Blood Shield (Supplicant)"
SUPPLICANT_SOUL_AUGMENTATION = "Soul Augmentation (Supplicant)"
SUPPLICANT_SHIELD_REGENERATION = "Shield Regeneration (Supplicant)"
ADEPT_SHOCKWAVE = "Shockwave (Adept)"
ADEPT_RESONATING_GLAIVES = "Resonating Glaives (Adept)"
ADEPT_PHASE_BULWARK = "Phase Bulwark (Adept)"
STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES = "Disintegrating Particles (Stalker/Instigator/Slayer)"
STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION = "Particle Reflection (Stalker/Instigator/Slayer)"
DRAGOON_HIGH_IMPACT_PHASE_DISRUPTORS = "High Impact Phase Disruptor (Dragoon)"
DRAGOON_TRILLIC_COMPRESSION_SYSTEM = "Trillic Compression System (Dragoon)"
DRAGOON_SINGULARITY_CHARGE = "Singularity Charge (Dragoon)"
DRAGOON_ENHANCED_STRIDER_SERVOS = "Enhanced Strider Servos (Dragoon)"
SCOUT_COMBAT_SENSOR_ARRAY = "Combat Sensor Array (Scout)"
SCOUT_APIAL_SENSORS = "Apial Sensors (Scout)"
SCOUT_GRAVITIC_THRUSTERS = "Gravitic Thrusters (Scout)"
SCOUT_ADVANCED_PHOTON_BLASTERS = "Advanced Photon Blasters (Scout)"
TEMPEST_TECTONIC_DESTABILIZERS = "Tectonic Destabilizers (Tempest)"
TEMPEST_QUANTIC_REACTOR = "Quantic Reactor (Tempest)"
TEMPEST_GRAVITY_SLING = "Gravity Sling (Tempest)"
PHOENIX_MIRAGE_IONIC_WAVELENGTH_FLUX = "Ionic Wavelength Flux (Phoenix/Mirage)"
PHOENIX_MIRAGE_ANION_PULSE_CRYSTALS = "Anion Pulse-Crystals (Phoenix/Mirage)"
CORSAIR_STEALTH_DRIVE = "Stealth Drive (Corsair)"
CORSAIR_ARGUS_JEWEL = "Argus Jewel (Corsair)"
CORSAIR_SUSTAINING_DISRUPTION = "Sustaining Disruption (Corsair)"
CORSAIR_NEUTRON_SHIELDS = "Neutron Shields (Corsair)"
ORACLE_STEALTH_DRIVE = "Stealth Drive (Oracle)"
ORACLE_STASIS_CALIBRATION = "Stasis Calibration (Oracle)"
ORACLE_TEMPORAL_ACCELERATION_BEAM = "Temporal Acceleration Beam (Oracle)"
ARBITER_CHRONOSTATIC_REINFORCEMENT = "Chronostatic Reinforcement (Arbiter)"
ARBITER_KHAYDARIN_CORE = "Khaydarin Core (Arbiter)"
ARBITER_SPACETIME_ANCHOR = "Spacetime Anchor (Arbiter)"
ARBITER_RESOURCE_EFFICIENCY = "Resource Efficiency (Arbiter)"
ARBITER_ENHANCED_CLOAK_FIELD = "Enhanced Cloak Field (Arbiter)"
CARRIER_GRAVITON_CATAPULT = "Graviton Catapult (Carrier)"
CARRIER_HULL_OF_PAST_GLORIES = "Hull of Past Glories (Carrier)"
VOID_RAY_DESTROYER_FLUX_VANES = "Flux Vanes (Void Ray/Destroyer)"
DESTROYER_REFORGED_BLOODSHARD_CORE = "Reforged Bloodshard Core (Destroyer)"
WARP_PRISM_GRAVITIC_DRIVE = "Gravitic Drive (Warp Prism)"
WARP_PRISM_PHASE_BLASTER = "Phase Blaster (Warp Prism)"
WARP_PRISM_WAR_CONFIGURATION = "War Configuration (Warp Prism)"
OBSERVER_GRAVITIC_BOOSTERS = "Gravitic Boosters (Observer)"
OBSERVER_SENSOR_ARRAY = "Sensor Array (Observer)"
REAVER_SCARAB_DAMAGE = "Scarab Damage (Reaver)"
REAVER_SOLARITE_PAYLOAD = "Solarite Payload (Reaver)"
REAVER_REAVER_CAPACITY = "Reaver Capacity (Reaver)"
REAVER_RESOURCE_EFFICIENCY = "Resource Efficiency (Reaver)"
VANGUARD_AGONY_LAUNCHERS = "Agony Launchers (Vanguard)"
VANGUARD_MATTER_DISPERSION = "Matter Dispersion (Vanguard)"
IMMORTAL_ANNIHILATOR_SINGULARITY_CHARGE = "Singularity Charge (Immortal/Annihilator)"
IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS = "Advanced Targeting Mechanics (Immortal/Annihilator)"
COLOSSUS_PACIFICATION_PROTOCOL = "Pacification Protocol (Colossus)"
WRATHWALKER_RAPID_POWER_CYCLING = "Rapid Power Cycling (Wrathwalker)"
WRATHWALKER_EYE_OF_WRATH = "Eye of Wrath (Wrathwalker)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHROUD_OF_ADUN = "Shroud of Adun (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHADOW_GUARD_TRAINING = "Shadow Guard Training (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK = "Blink (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_RESOURCE_EFFICIENCY = "Resource Efficiency (Dark Templar/Avenger/Blood Hunter)"
DARK_TEMPLAR_DARK_ARCHON_MELD = "Dark Archon Meld (Dark Templar)"
HIGH_TEMPLAR_SIGNIFIER_UNSHACKLED_PSIONIC_STORM = "Unshackled Psionic Storm (High Templar/Signifier)"
HIGH_TEMPLAR_SIGNIFIER_HALLUCINATION = "Hallucination (High Templar/Signifier)"
HIGH_TEMPLAR_SIGNIFIER_KHAYDARIN_AMULET = "Khaydarin Amulet (High Templar/Signifier)"
ARCHON_HIGH_ARCHON = "High Archon (Archon)"
DARK_ARCHON_FEEDBACK = "Feedback (Dark Archon)"
DARK_ARCHON_MAELSTROM = "Maelstrom (Dark Archon)"
DARK_ARCHON_ARGUS_TALISMAN = "Argus Talisman (Dark Archon)"
ASCENDANT_POWER_OVERWHELMING = "Power Overwhelming (Ascendant)"
ASCENDANT_CHAOTIC_ATTUNEMENT = "Chaotic Attunement (Ascendant)"
ASCENDANT_BLOOD_AMULET = "Blood Amulet (Ascendant)"
SENTRY_ENERGIZER_HAVOC_CLOAKING_MODULE = "Cloaking Module (Sentry/Energizer/Havoc)"
SENTRY_ENERGIZER_HAVOC_SHIELD_BATTERY_RAPID_RECHARGING = "Rapid Recharging (Sentry/Energizer/Havoc/Shield Battery)"
SENTRY_FORCE_FIELD = "Force Field (Sentry)"
SENTRY_HALLUCINATION = "Hallucination (Sentry)"
ENERGIZER_RECLAMATION = "Reclamation (Energizer)"
ENERGIZER_FORGED_CHASSIS = "Forged Chassis (Energizer)"
HAVOC_DETECT_WEAKNESS = "Detect Weakness (Havoc)"
HAVOC_BLOODSHARD_RESONANCE = "Bloodshard Resonance (Havoc)"
ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS = "Leg Enhancements (Zealot/Sentinel/Centurion)"
ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY = "Shield Capacity (Zealot/Sentinel/Centurion)"
# Spear Of Adun
SOA_CHRONO_SURGE = "Chrono Surge (Spear of Adun Calldown)"
SOA_PROGRESSIVE_PROXY_PYLON = "Progressive Proxy Pylon (Spear of Adun Calldown)"
SOA_PYLON_OVERCHARGE = "Pylon Overcharge (Spear of Adun Calldown)"
SOA_ORBITAL_STRIKE = "Orbital Strike (Spear of Adun Calldown)"
SOA_TEMPORAL_FIELD = "Temporal Field (Spear of Adun Calldown)"
SOA_SOLAR_LANCE = "Solar Lance (Spear of Adun Calldown)"
SOA_MASS_RECALL = "Mass Recall (Spear of Adun Calldown)"
SOA_SHIELD_OVERCHARGE = "Shield Overcharge (Spear of Adun Calldown)"
SOA_DEPLOY_FENIX = "Deploy Fenix (Spear of Adun Calldown)"
SOA_PURIFIER_BEAM = "Purifier Beam (Spear of Adun Calldown)"
SOA_TIME_STOP = "Time Stop (Spear of Adun Calldown)"
SOA_SOLAR_BOMBARDMENT = "Solar Bombardment (Spear of Adun Calldown)"
# Generic upgrades
MATRIX_OVERLOAD = "Matrix Overload"
QUATRO = "Quatro"
NEXUS_OVERCHARGE = "Nexus Overcharge"
ORBITAL_ASSIMILATORS = "Orbital Assimilators"
WARP_HARMONIZATION = "Warp Harmonization"
GUARDIAN_SHELL = "Guardian Shell"
RECONSTRUCTION_BEAM = "Reconstruction Beam (Spear of Adun Auto-Cast)"
OVERWATCH = "Overwatch (Spear of Adun Auto-Cast)"
SUPERIOR_WARP_GATES = "Superior Warp Gates"
ENHANCED_TARGETING = "Enhanced Targeting"
OPTIMIZED_ORDNANCE = "Optimized Ordnance"
KHALAI_INGENUITY = "Khalai Ingenuity"
AMPLIFIED_ASSIMILATORS = "Amplified Assimilators"
# Filler items
STARTING_MINERALS = "Additional Starting Minerals"
STARTING_VESPENE = "Additional Starting Vespene"
STARTING_SUPPLY = "Additional Starting Supply"
NOTHING = "Nothing"

2553
worlds/sc2/Items.py Normal file

File diff suppressed because it is too large Load Diff

1638
worlds/sc2/Locations.py Normal file

File diff suppressed because it is too large Load Diff

737
worlds/sc2/MissionTables.py Normal file
View File

@ -0,0 +1,737 @@
from typing import NamedTuple, Dict, List, Set, Union, Literal, Iterable, Callable
from enum import IntEnum, Enum
class SC2Race(IntEnum):
ANY = 0
TERRAN = 1
ZERG = 2
PROTOSS = 3
class MissionPools(IntEnum):
STARTER = 0
EASY = 1
MEDIUM = 2
HARD = 3
VERY_HARD = 4
FINAL = 5
class SC2CampaignGoalPriority(IntEnum):
"""
Campaign's priority to goal election
"""
NONE = 0
MINI_CAMPAIGN = 1 # A goal shouldn't be in a mini-campaign if there's at least one 'big' campaign
HARD = 2 # A campaign ending with a hard mission
VERY_HARD = 3 # A campaign ending with a very hard mission
EPILOGUE = 4 # Epilogue shall be always preferred as the goal if present
class SC2Campaign(Enum):
def __new__(cls, *args, **kwargs):
value = len(cls.__members__) + 1
obj = object.__new__(cls)
obj._value_ = value
return obj
def __init__(self, campaign_id: int, name: str, goal_priority: SC2CampaignGoalPriority, race: SC2Race):
self.id = campaign_id
self.campaign_name = name
self.goal_priority = goal_priority
self.race = race
GLOBAL = 0, "Global", SC2CampaignGoalPriority.NONE, SC2Race.ANY
WOL = 1, "Wings of Liberty", SC2CampaignGoalPriority.VERY_HARD, SC2Race.TERRAN
PROPHECY = 2, "Prophecy", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS
HOTS = 3, "Heart of the Swarm", SC2CampaignGoalPriority.HARD, SC2Race.ZERG
PROLOGUE = 4, "Whispers of Oblivion (Legacy of the Void: Prologue)", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS
LOTV = 5, "Legacy of the Void", SC2CampaignGoalPriority.VERY_HARD, SC2Race.PROTOSS
EPILOGUE = 6, "Into the Void (Legacy of the Void: Epilogue)", SC2CampaignGoalPriority.EPILOGUE, SC2Race.ANY
NCO = 7, "Nova Covert Ops", SC2CampaignGoalPriority.HARD, SC2Race.TERRAN
class SC2Mission(Enum):
def __new__(cls, *args, **kwargs):
value = len(cls.__members__) + 1
obj = object.__new__(cls)
obj._value_ = value
return obj
def __init__(self, mission_id: int, name: str, campaign: SC2Campaign, area: str, race: SC2Race, pool: MissionPools, map_file: str, build: bool = True):
self.id = mission_id
self.mission_name = name
self.campaign = campaign
self.area = area
self.race = race
self.pool = pool
self.map_file = map_file
self.build = build
# Wings of Liberty
LIBERATION_DAY = 1, "Liberation Day", SC2Campaign.WOL, "Mar Sara", SC2Race.ANY, MissionPools.STARTER, "ap_liberation_day", False
THE_OUTLAWS = 2, "The Outlaws", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_the_outlaws"
ZERO_HOUR = 3, "Zero Hour", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_zero_hour"
EVACUATION = 4, "Evacuation", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_evacuation"
OUTBREAK = 5, "Outbreak", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_outbreak"
SAFE_HAVEN = 6, "Safe Haven", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_safe_haven"
HAVENS_FALL = 7, "Haven's Fall", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_havens_fall"
SMASH_AND_GRAB = 8, "Smash and Grab", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.EASY, "ap_smash_and_grab"
THE_DIG = 9, "The Dig", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_dig"
THE_MOEBIUS_FACTOR = 10, "The Moebius Factor", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_moebius_factor"
SUPERNOVA = 11, "Supernova", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.HARD, "ap_supernova"
MAW_OF_THE_VOID = 12, "Maw of the Void", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.HARD, "ap_maw_of_the_void"
DEVILS_PLAYGROUND = 13, "Devil's Playground", SC2Campaign.WOL, "Covert", SC2Race.TERRAN, MissionPools.EASY, "ap_devils_playground"
WELCOME_TO_THE_JUNGLE = 14, "Welcome to the Jungle", SC2Campaign.WOL, "Covert", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_welcome_to_the_jungle"
BREAKOUT = 15, "Breakout", SC2Campaign.WOL, "Covert", SC2Race.ANY, MissionPools.STARTER, "ap_breakout", False
GHOST_OF_A_CHANCE = 16, "Ghost of a Chance", SC2Campaign.WOL, "Covert", SC2Race.ANY, MissionPools.STARTER, "ap_ghost_of_a_chance", False
THE_GREAT_TRAIN_ROBBERY = 17, "The Great Train Robbery", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_great_train_robbery"
CUTTHROAT = 18, "Cutthroat", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_cutthroat"
ENGINE_OF_DESTRUCTION = 19, "Engine of Destruction", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.HARD, "ap_engine_of_destruction"
MEDIA_BLITZ = 20, "Media Blitz", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_media_blitz"
PIERCING_OF_THE_SHROUD = 21, "Piercing the Shroud", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.STARTER, "ap_piercing_the_shroud", False
GATES_OF_HELL = 26, "Gates of Hell", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.HARD, "ap_gates_of_hell"
BELLY_OF_THE_BEAST = 27, "Belly of the Beast", SC2Campaign.WOL, "Char", SC2Race.ANY, MissionPools.STARTER, "ap_belly_of_the_beast", False
SHATTER_THE_SKY = 28, "Shatter the Sky", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.HARD, "ap_shatter_the_sky"
ALL_IN = 29, "All-In", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_all_in"
# Prophecy
WHISPERS_OF_DOOM = 22, "Whispers of Doom", SC2Campaign.PROPHECY, "_1", SC2Race.ANY, MissionPools.STARTER, "ap_whispers_of_doom", False
A_SINISTER_TURN = 23, "A Sinister Turn", SC2Campaign.PROPHECY, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_a_sinister_turn"
ECHOES_OF_THE_FUTURE = 24, "Echoes of the Future", SC2Campaign.PROPHECY, "_3", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_echoes_of_the_future"
IN_UTTER_DARKNESS = 25, "In Utter Darkness", SC2Campaign.PROPHECY, "_4", SC2Race.PROTOSS, MissionPools.HARD, "ap_in_utter_darkness"
# Heart of the Swarm
LAB_RAT = 30, "Lab Rat", SC2Campaign.HOTS, "Umoja", SC2Race.ZERG, MissionPools.STARTER, "ap_lab_rat"
BACK_IN_THE_SADDLE = 31, "Back in the Saddle", SC2Campaign.HOTS, "Umoja", SC2Race.ANY, MissionPools.STARTER, "ap_back_in_the_saddle", False
RENDEZVOUS = 32, "Rendezvous", SC2Campaign.HOTS, "Umoja", SC2Race.ZERG, MissionPools.EASY, "ap_rendezvous"
HARVEST_OF_SCREAMS = 33, "Harvest of Screams", SC2Campaign.HOTS, "Kaldir", SC2Race.ZERG, MissionPools.EASY, "ap_harvest_of_screams"
SHOOT_THE_MESSENGER = 34, "Shoot the Messenger", SC2Campaign.HOTS, "Kaldir", SC2Race.ZERG, MissionPools.EASY, "ap_shoot_the_messenger"
ENEMY_WITHIN = 35, "Enemy Within", SC2Campaign.HOTS, "Kaldir", SC2Race.ANY, MissionPools.EASY, "ap_enemy_within", False
DOMINATION = 36, "Domination", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.EASY, "ap_domination"
FIRE_IN_THE_SKY = 37, "Fire in the Sky", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.MEDIUM, "ap_fire_in_the_sky"
OLD_SOLDIERS = 38, "Old Soldiers", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.MEDIUM, "ap_old_soldiers"
WAKING_THE_ANCIENT = 39, "Waking the Ancient", SC2Campaign.HOTS, "Zerus", SC2Race.ZERG, MissionPools.MEDIUM, "ap_waking_the_ancient"
THE_CRUCIBLE = 40, "The Crucible", SC2Campaign.HOTS, "Zerus", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_crucible"
SUPREME = 41, "Supreme", SC2Campaign.HOTS, "Zerus", SC2Race.ANY, MissionPools.MEDIUM, "ap_supreme", False
INFESTED = 42, "Infested", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.MEDIUM, "ap_infested"
HAND_OF_DARKNESS = 43, "Hand of Darkness", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.HARD, "ap_hand_of_darkness"
PHANTOMS_OF_THE_VOID = 44, "Phantoms of the Void", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.HARD, "ap_phantoms_of_the_void"
WITH_FRIENDS_LIKE_THESE = 45, "With Friends Like These", SC2Campaign.HOTS, "Dominion Space", SC2Race.ANY, MissionPools.STARTER, "ap_with_friends_like_these", False
CONVICTION = 46, "Conviction", SC2Campaign.HOTS, "Dominion Space", SC2Race.ANY, MissionPools.MEDIUM, "ap_conviction", False
PLANETFALL = 47, "Planetfall", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_planetfall"
DEATH_FROM_ABOVE = 48, "Death From Above", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_death_from_above"
THE_RECKONING = 49, "The Reckoning", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_the_reckoning"
# Prologue
DARK_WHISPERS = 50, "Dark Whispers", SC2Campaign.PROLOGUE, "_1", SC2Race.PROTOSS, MissionPools.EASY, "ap_dark_whispers"
GHOSTS_IN_THE_FOG = 51, "Ghosts in the Fog", SC2Campaign.PROLOGUE, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_ghosts_in_the_fog"
EVIL_AWOKEN = 52, "Evil Awoken", SC2Campaign.PROLOGUE, "_3", SC2Race.PROTOSS, MissionPools.STARTER, "ap_evil_awoken", False
# LotV
FOR_AIUR = 53, "For Aiur!", SC2Campaign.LOTV, "Aiur", SC2Race.ANY, MissionPools.STARTER, "ap_for_aiur", False
THE_GROWING_SHADOW = 54, "The Growing Shadow", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_growing_shadow"
THE_SPEAR_OF_ADUN = 55, "The Spear of Adun", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_spear_of_adun"
SKY_SHIELD = 56, "Sky Shield", SC2Campaign.LOTV, "Korhal", SC2Race.PROTOSS, MissionPools.EASY, "ap_sky_shield"
BROTHERS_IN_ARMS = 57, "Brothers in Arms", SC2Campaign.LOTV, "Korhal", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_brothers_in_arms"
AMON_S_REACH = 58, "Amon's Reach", SC2Campaign.LOTV, "Shakuras", SC2Race.PROTOSS, MissionPools.EASY, "ap_amon_s_reach"
LAST_STAND = 59, "Last Stand", SC2Campaign.LOTV, "Shakuras", SC2Race.PROTOSS, MissionPools.HARD, "ap_last_stand"
FORBIDDEN_WEAPON = 60, "Forbidden Weapon", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_forbidden_weapon"
TEMPLE_OF_UNIFICATION = 61, "Temple of Unification", SC2Campaign.LOTV, "Ulnar", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_temple_of_unification"
THE_INFINITE_CYCLE = 62, "The Infinite Cycle", SC2Campaign.LOTV, "Ulnar", SC2Race.ANY, MissionPools.HARD, "ap_the_infinite_cycle", False
HARBINGER_OF_OBLIVION = 63, "Harbinger of Oblivion", SC2Campaign.LOTV, "Ulnar", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_harbinger_of_oblivion"
UNSEALING_THE_PAST = 64, "Unsealing the Past", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_unsealing_the_past"
PURIFICATION = 65, "Purification", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.HARD, "ap_purification"
STEPS_OF_THE_RITE = 66, "Steps of the Rite", SC2Campaign.LOTV, "Tal'darim", SC2Race.PROTOSS, MissionPools.HARD, "ap_steps_of_the_rite"
RAK_SHIR = 67, "Rak'Shir", SC2Campaign.LOTV, "Tal'darim", SC2Race.PROTOSS, MissionPools.HARD, "ap_rak_shir"
TEMPLAR_S_CHARGE = 68, "Templar's Charge", SC2Campaign.LOTV, "Moebius", SC2Race.PROTOSS, MissionPools.HARD, "ap_templar_s_charge"
TEMPLAR_S_RETURN = 69, "Templar's Return", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_templar_s_return", False
THE_HOST = 70, "The Host", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.HARD, "ap_the_host",
SALVATION = 71, "Salvation", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_salvation"
# Epilogue
INTO_THE_VOID = 72, "Into the Void", SC2Campaign.EPILOGUE, "_1", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_into_the_void"
THE_ESSENCE_OF_ETERNITY = 73, "The Essence of Eternity", SC2Campaign.EPILOGUE, "_2", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_the_essence_of_eternity"
AMON_S_FALL = 74, "Amon's Fall", SC2Campaign.EPILOGUE, "_3", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_amon_s_fall"
# Nova Covert Ops
THE_ESCAPE = 75, "The Escape", SC2Campaign.NCO, "_1", SC2Race.ANY, MissionPools.MEDIUM, "ap_the_escape", False
SUDDEN_STRIKE = 76, "Sudden Strike", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.EASY, "ap_sudden_strike"
ENEMY_INTELLIGENCE = 77, "Enemy Intelligence", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_enemy_intelligence"
TROUBLE_IN_PARADISE = 78, "Trouble In Paradise", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_trouble_in_paradise"
NIGHT_TERRORS = 79, "Night Terrors", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_night_terrors"
FLASHPOINT = 80, "Flashpoint", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_flashpoint"
IN_THE_ENEMY_S_SHADOW = 81, "In the Enemy's Shadow", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_in_the_enemy_s_shadow", False
DARK_SKIES = 82, "Dark Skies", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.HARD, "ap_dark_skies"
END_GAME = 83, "End Game", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_end_game"
class MissionConnection:
campaign: SC2Campaign
connect_to: int # -1 connects to Menu
def __init__(self, connect_to, campaign = SC2Campaign.GLOBAL):
self.campaign = campaign
self.connect_to = connect_to
def _asdict(self):
return {
"campaign": self.campaign.id,
"connect_to": self.connect_to
}
class MissionInfo(NamedTuple):
mission: SC2Mission
required_world: List[Union[MissionConnection, Dict[Literal["campaign", "connect_to"], int]]]
category: str
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
ui_vertical_padding: int = 0
class FillMission(NamedTuple):
type: MissionPools
connect_to: List[MissionConnection]
category: str
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
removal_priority: int = 0 # how many missions missing from the pool required to remove this mission
def vanilla_shuffle_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.WOL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.WOL)], "Colonist"),
FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.WOL)], "Colonist"),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.WOL)], "Colonist", number=7),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.WOL)], "Colonist", number=7, removal_priority=1),
FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.WOL)], "Artifact", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(7, SC2Campaign.WOL)], "Artifact", number=8, completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(8, SC2Campaign.WOL)], "Artifact", number=11, completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(9, SC2Campaign.WOL)], "Artifact", number=14, completion_critical=True, removal_priority=7),
FillMission(MissionPools.HARD, [MissionConnection(10, SC2Campaign.WOL)], "Artifact", completion_critical=True, removal_priority=6),
FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.WOL)], "Covert", number=4),
FillMission(MissionPools.MEDIUM, [MissionConnection(12, SC2Campaign.WOL)], "Covert"),
FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.WOL)], "Covert", number=8, removal_priority=3),
FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.WOL)], "Covert", number=8, removal_priority=2),
FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.WOL)], "Rebellion", number=6),
FillMission(MissionPools.HARD, [MissionConnection(16, SC2Campaign.WOL)], "Rebellion"),
FillMission(MissionPools.HARD, [MissionConnection(17, SC2Campaign.WOL)], "Rebellion"),
FillMission(MissionPools.HARD, [MissionConnection(18, SC2Campaign.WOL)], "Rebellion", removal_priority=8),
FillMission(MissionPools.HARD, [MissionConnection(19, SC2Campaign.WOL)], "Rebellion", removal_priority=5),
FillMission(MissionPools.HARD, [MissionConnection(11, SC2Campaign.WOL)], "Char", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(21, SC2Campaign.WOL)], "Char", completion_critical=True, removal_priority=4),
FillMission(MissionPools.HARD, [MissionConnection(21, SC2Campaign.WOL)], "Char", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(22, SC2Campaign.WOL), MissionConnection(23, SC2Campaign.WOL)], "Char", completion_critical=True, or_requirements=True)
],
SC2Campaign.PROPHECY: [
FillMission(MissionPools.MEDIUM, [MissionConnection(8, SC2Campaign.WOL)], "_1"),
FillMission(MissionPools.HARD, [MissionConnection(0, SC2Campaign.PROPHECY)], "_2", removal_priority=2),
FillMission(MissionPools.HARD, [MissionConnection(1, SC2Campaign.PROPHECY)], "_3", removal_priority=1),
FillMission(MissionPools.FINAL, [MissionConnection(2, SC2Campaign.PROPHECY)], "_4"),
],
SC2Campaign.HOTS: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.HOTS)], "Umoja", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.HOTS)], "Umoja", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.HOTS)], "Umoja", completion_critical=True, removal_priority=1),
FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.HOTS)], "Kaldir", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.HOTS)], "Kaldir", completion_critical=True, removal_priority=2),
FillMission(MissionPools.MEDIUM, [MissionConnection(4, SC2Campaign.HOTS)], "Kaldir", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.HOTS)], "Char", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(6, SC2Campaign.HOTS)], "Char", completion_critical=True, removal_priority=3),
FillMission(MissionPools.MEDIUM, [MissionConnection(7, SC2Campaign.HOTS)], "Char", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS), MissionConnection(8, SC2Campaign.HOTS)], "Zerus", completion_critical=True, or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(9, SC2Campaign.HOTS)], "Zerus", completion_critical=True, removal_priority=4),
FillMission(MissionPools.MEDIUM, [MissionConnection(10, SC2Campaign.HOTS)], "Zerus", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS), MissionConnection(8, SC2Campaign.HOTS), MissionConnection(11, SC2Campaign.HOTS)], "Skygeirr Station", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(12, SC2Campaign.HOTS)], "Skygeirr Station", completion_critical=True, removal_priority=5),
FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.HOTS)], "Skygeirr Station", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS), MissionConnection(8, SC2Campaign.HOTS), MissionConnection(11, SC2Campaign.HOTS)], "Dominion Space", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(15, SC2Campaign.HOTS)], "Dominion Space", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(14, SC2Campaign.HOTS), MissionConnection(16, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(17, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(18, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
],
SC2Campaign.PROLOGUE: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.PROLOGUE)], "_1"),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.PROLOGUE)], "_2", removal_priority=1),
FillMission(MissionPools.FINAL, [MissionConnection(1, SC2Campaign.PROLOGUE)], "_3")
],
SC2Campaign.LOTV: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.LOTV)], "Aiur", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.LOTV)], "Aiur", completion_critical=True, removal_priority=3),
FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.LOTV)], "Aiur", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.LOTV)], "Korhal", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.LOTV)], "Korhal", completion_critical=True, removal_priority=7),
FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.LOTV)], "Shakuras", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.LOTV)], "Shakuras", completion_critical=True, removal_priority=6),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.LOTV), MissionConnection(6, SC2Campaign.LOTV)], "Purifier", completion_critical=True, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.LOTV), MissionConnection(6, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV)], "Ulnar", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(8, SC2Campaign.LOTV)], "Ulnar", completion_critical=True, removal_priority=1),
FillMission(MissionPools.HARD, [MissionConnection(9, SC2Campaign.LOTV)], "Ulnar", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(10, SC2Campaign.LOTV)], "Purifier", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(11, SC2Campaign.LOTV)], "Purifier", completion_critical=True, removal_priority=5),
FillMission(MissionPools.HARD, [MissionConnection(10, SC2Campaign.LOTV)], "Tal'darim", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.LOTV)], "Tal'darim", completion_critical=True, removal_priority=4),
FillMission(MissionPools.HARD, [MissionConnection(12, SC2Campaign.LOTV), MissionConnection(14, SC2Campaign.LOTV)], "Moebius", completion_critical=True, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(12, SC2Campaign.LOTV), MissionConnection(14, SC2Campaign.LOTV), MissionConnection(15, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(16, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True, removal_priority=2),
FillMission(MissionPools.FINAL, [MissionConnection(17, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
],
SC2Campaign.EPILOGUE: [
FillMission(MissionPools.VERY_HARD, [MissionConnection(24, SC2Campaign.WOL), MissionConnection(19, SC2Campaign.HOTS), MissionConnection(18, SC2Campaign.LOTV)], "_1", completion_critical=True),
FillMission(MissionPools.VERY_HARD, [MissionConnection(0, SC2Campaign.EPILOGUE)], "_2", completion_critical=True, removal_priority=1),
FillMission(MissionPools.FINAL, [MissionConnection(1, SC2Campaign.EPILOGUE)], "_3", completion_critical=True),
],
SC2Campaign.NCO: [
FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.NCO)], "_1", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.NCO)], "_1", completion_critical=True, removal_priority=6),
FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.NCO)], "_1", completion_critical=True, removal_priority=5),
FillMission(MissionPools.HARD, [MissionConnection(2, SC2Campaign.NCO)], "_2", completion_critical=True, removal_priority=7),
FillMission(MissionPools.HARD, [MissionConnection(3, SC2Campaign.NCO)], "_2", completion_critical=True, removal_priority=4),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.NCO)], "_2", completion_critical=True, removal_priority=3),
FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.NCO)], "_3", completion_critical=True, removal_priority=2),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.NCO)], "_3", completion_critical=True, removal_priority=1),
FillMission(MissionPools.FINAL, [MissionConnection(7, SC2Campaign.NCO)], "_3", completion_critical=True),
]
}
def mini_campaign_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.WOL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.WOL)], "Colonist"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.WOL)], "Colonist"),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.WOL)], "Artifact", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.WOL)], "Artifact", number=4, completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.WOL)], "Artifact", number=8, completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.WOL)], "Covert", number=2),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.WOL)], "Covert"),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.WOL)], "Rebellion", number=3),
FillMission(MissionPools.HARD, [MissionConnection(8, SC2Campaign.WOL)], "Rebellion"),
FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.WOL)], "Char", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.WOL)], "Char", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(10, SC2Campaign.WOL), MissionConnection(11, SC2Campaign.WOL)], "Char", completion_critical=True, or_requirements=True)
],
SC2Campaign.PROPHECY: [
FillMission(MissionPools.MEDIUM, [MissionConnection(4, SC2Campaign.WOL)], "_1"),
FillMission(MissionPools.FINAL, [MissionConnection(0, SC2Campaign.PROPHECY)], "_2"),
],
SC2Campaign.HOTS: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.HOTS)], "Umoja", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.HOTS)], "Kaldir"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.HOTS)], "Kaldir"),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.HOTS)], "Char"),
FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.HOTS)], "Char"),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.HOTS)], "Zerus", number=3),
FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS)], "Zerus"),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.HOTS)], "Skygeirr Station", number=5),
FillMission(MissionPools.HARD, [MissionConnection(7, SC2Campaign.HOTS)], "Skygeirr Station"),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.HOTS)], "Dominion Space", number=5),
FillMission(MissionPools.HARD, [MissionConnection(9, SC2Campaign.HOTS)], "Dominion Space"),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.HOTS)], "Korhal", completion_critical=True, number=8),
FillMission(MissionPools.FINAL, [MissionConnection(11, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
],
SC2Campaign.PROLOGUE: [
FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.PROLOGUE)], "_1"),
FillMission(MissionPools.FINAL, [MissionConnection(0, SC2Campaign.PROLOGUE)], "_2")
],
SC2Campaign.LOTV: [
FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.LOTV)], "Aiur",completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.LOTV)], "Aiur", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.LOTV)], "Korhal", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.LOTV)], "Shakuras", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.LOTV), MissionConnection(3, SC2Campaign.LOTV)], "Purifier", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.LOTV)], "Purifier", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.LOTV)], "Ulnar", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.LOTV)], "Tal'darim", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(8, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
],
SC2Campaign.EPILOGUE: [
FillMission(MissionPools.VERY_HARD, [MissionConnection(12, SC2Campaign.WOL), MissionConnection(12, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.LOTV)], "_1", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(0, SC2Campaign.EPILOGUE)], "_2", completion_critical=True),
],
SC2Campaign.NCO: [
FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.NCO)], "_1", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.NCO)], "_1", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.NCO)], "_2", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(2, SC2Campaign.NCO)], "_3", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(3, SC2Campaign.NCO)], "_3", completion_critical=True),
]
}
def gauntlet_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "I", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0)], "II", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(1)], "III", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(2)], "IV", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(3)], "V", completion_critical=True),
FillMission(MissionPools.HARD, [MissionConnection(4)], "VI", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(5)], "Final", completion_critical=True)
]
}
def mini_gauntlet_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "I", completion_critical=True),
FillMission(MissionPools.EASY, [MissionConnection(0)], "II", completion_critical=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(1)], "III", completion_critical=True),
FillMission(MissionPools.FINAL, [MissionConnection(2)], "Final", completion_critical=True)
]
}
def grid_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "_1"),
FillMission(MissionPools.EASY, [MissionConnection(0)], "_1"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(6), MissionConnection( 3)], "_1", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(2), MissionConnection(7)], "_1", or_requirements=True),
FillMission(MissionPools.EASY, [MissionConnection(0)], "_2"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(4)], "_2", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(2), MissionConnection(5), MissionConnection(10), MissionConnection(7)], "_2", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(3), MissionConnection(6), MissionConnection(11)], "_2", or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(4), MissionConnection(9), MissionConnection(12)], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(5), MissionConnection(8), MissionConnection(10), MissionConnection(13)], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(6), MissionConnection(9), MissionConnection(11), MissionConnection(14)], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(7), MissionConnection(10)], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(8), MissionConnection(13)], "_4", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(9), MissionConnection(12), MissionConnection(14)], "_4", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(10), MissionConnection(13)], "_4", or_requirements=True),
FillMission(MissionPools.FINAL, [MissionConnection(11), MissionConnection(14)], "_4", or_requirements=True)
]
}
def mini_grid_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "_1"),
FillMission(MissionPools.EASY, [MissionConnection(0)], "_1"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(5)], "_1", or_requirements=True),
FillMission(MissionPools.EASY, [MissionConnection(0)], "_2"),
FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(3)], "_2", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(2), MissionConnection(4)], "_2", or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(3), MissionConnection(7)], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(4), MissionConnection(6)], "_3", or_requirements=True),
FillMission(MissionPools.FINAL, [MissionConnection(5), MissionConnection(7)], "_3", or_requirements=True)
]
}
def tiny_grid_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "_1"),
FillMission(MissionPools.MEDIUM, [MissionConnection(0)], "_1"),
FillMission(MissionPools.EASY, [MissionConnection(0)], "_2"),
FillMission(MissionPools.FINAL, [MissionConnection(1), MissionConnection(2)], "_2", or_requirements=True),
]
}
def blitz_order() -> Dict[SC2Campaign, List[FillMission]]:
return {
SC2Campaign.GLOBAL: [
FillMission(MissionPools.STARTER, [MissionConnection(-1)], "I"),
FillMission(MissionPools.EASY, [MissionConnection(-1)], "I"),
FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "II", number=1, or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "II", number=1, or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "III", number=2, or_requirements=True),
FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "III", number=2, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "IV", number=3, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "IV", number=3, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "V", number=4, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "V", number=4, or_requirements=True),
FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "Final", number=5, or_requirements=True),
FillMission(MissionPools.FINAL, [MissionConnection(0), MissionConnection(1)], "Final", number=5, or_requirements=True)
]
}
mission_orders: List[Callable[[], Dict[SC2Campaign, List[FillMission]]]] = [
vanilla_shuffle_order,
vanilla_shuffle_order,
mini_campaign_order,
grid_order,
mini_grid_order,
blitz_order,
gauntlet_order,
mini_gauntlet_order,
tiny_grid_order
]
vanilla_mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = {
SC2Campaign.WOL: {
SC2Mission.LIBERATION_DAY.mission_name: MissionInfo(SC2Mission.LIBERATION_DAY, [], SC2Mission.LIBERATION_DAY.area, completion_critical=True),
SC2Mission.THE_OUTLAWS.mission_name: MissionInfo(SC2Mission.THE_OUTLAWS, [MissionConnection(1, SC2Campaign.WOL)], SC2Mission.THE_OUTLAWS.area, completion_critical=True),
SC2Mission.ZERO_HOUR.mission_name: MissionInfo(SC2Mission.ZERO_HOUR, [MissionConnection(2, SC2Campaign.WOL)], SC2Mission.ZERO_HOUR.area, completion_critical=True),
SC2Mission.EVACUATION.mission_name: MissionInfo(SC2Mission.EVACUATION, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.EVACUATION.area),
SC2Mission.OUTBREAK.mission_name: MissionInfo(SC2Mission.OUTBREAK, [MissionConnection(4, SC2Campaign.WOL)], SC2Mission.OUTBREAK.area),
SC2Mission.SAFE_HAVEN.mission_name: MissionInfo(SC2Mission.SAFE_HAVEN, [MissionConnection(5, SC2Campaign.WOL)], SC2Mission.SAFE_HAVEN.area, number=7),
SC2Mission.HAVENS_FALL.mission_name: MissionInfo(SC2Mission.HAVENS_FALL, [MissionConnection(5, SC2Campaign.WOL)], SC2Mission.HAVENS_FALL.area, number=7),
SC2Mission.SMASH_AND_GRAB.mission_name: MissionInfo(SC2Mission.SMASH_AND_GRAB, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.SMASH_AND_GRAB.area, completion_critical=True),
SC2Mission.THE_DIG.mission_name: MissionInfo(SC2Mission.THE_DIG, [MissionConnection(8, SC2Campaign.WOL)], SC2Mission.THE_DIG.area, number=8, completion_critical=True),
SC2Mission.THE_MOEBIUS_FACTOR.mission_name: MissionInfo(SC2Mission.THE_MOEBIUS_FACTOR, [MissionConnection(9, SC2Campaign.WOL)], SC2Mission.THE_MOEBIUS_FACTOR.area, number=11, completion_critical=True),
SC2Mission.SUPERNOVA.mission_name: MissionInfo(SC2Mission.SUPERNOVA, [MissionConnection(10, SC2Campaign.WOL)], SC2Mission.SUPERNOVA.area, number=14, completion_critical=True),
SC2Mission.MAW_OF_THE_VOID.mission_name: MissionInfo(SC2Mission.MAW_OF_THE_VOID, [MissionConnection(11, SC2Campaign.WOL)], SC2Mission.MAW_OF_THE_VOID.area, completion_critical=True),
SC2Mission.DEVILS_PLAYGROUND.mission_name: MissionInfo(SC2Mission.DEVILS_PLAYGROUND, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.DEVILS_PLAYGROUND.area, number=4),
SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name: MissionInfo(SC2Mission.WELCOME_TO_THE_JUNGLE, [MissionConnection(13, SC2Campaign.WOL)], SC2Mission.WELCOME_TO_THE_JUNGLE.area),
SC2Mission.BREAKOUT.mission_name: MissionInfo(SC2Mission.BREAKOUT, [MissionConnection(14, SC2Campaign.WOL)], SC2Mission.BREAKOUT.area, number=8),
SC2Mission.GHOST_OF_A_CHANCE.mission_name: MissionInfo(SC2Mission.GHOST_OF_A_CHANCE, [MissionConnection(14, SC2Campaign.WOL)], SC2Mission.GHOST_OF_A_CHANCE.area, number=8),
SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name: MissionInfo(SC2Mission.THE_GREAT_TRAIN_ROBBERY, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.THE_GREAT_TRAIN_ROBBERY.area, number=6),
SC2Mission.CUTTHROAT.mission_name: MissionInfo(SC2Mission.CUTTHROAT, [MissionConnection(17, SC2Campaign.WOL)], SC2Mission.THE_GREAT_TRAIN_ROBBERY.area),
SC2Mission.ENGINE_OF_DESTRUCTION.mission_name: MissionInfo(SC2Mission.ENGINE_OF_DESTRUCTION, [MissionConnection(18, SC2Campaign.WOL)], SC2Mission.ENGINE_OF_DESTRUCTION.area),
SC2Mission.MEDIA_BLITZ.mission_name: MissionInfo(SC2Mission.MEDIA_BLITZ, [MissionConnection(19, SC2Campaign.WOL)], SC2Mission.MEDIA_BLITZ.area),
SC2Mission.PIERCING_OF_THE_SHROUD.mission_name: MissionInfo(SC2Mission.PIERCING_OF_THE_SHROUD, [MissionConnection(20, SC2Campaign.WOL)], SC2Mission.PIERCING_OF_THE_SHROUD.area),
SC2Mission.GATES_OF_HELL.mission_name: MissionInfo(SC2Mission.GATES_OF_HELL, [MissionConnection(12, SC2Campaign.WOL)], SC2Mission.GATES_OF_HELL.area, completion_critical=True),
SC2Mission.BELLY_OF_THE_BEAST.mission_name: MissionInfo(SC2Mission.BELLY_OF_THE_BEAST, [MissionConnection(22, SC2Campaign.WOL)], SC2Mission.BELLY_OF_THE_BEAST.area, completion_critical=True),
SC2Mission.SHATTER_THE_SKY.mission_name: MissionInfo(SC2Mission.SHATTER_THE_SKY, [MissionConnection(22, SC2Campaign.WOL)], SC2Mission.SHATTER_THE_SKY.area, completion_critical=True),
SC2Mission.ALL_IN.mission_name: MissionInfo(SC2Mission.ALL_IN, [MissionConnection(23, SC2Campaign.WOL), MissionConnection(24, SC2Campaign.WOL)], SC2Mission.ALL_IN.area, or_requirements=True, completion_critical=True)
},
SC2Campaign.PROPHECY: {
SC2Mission.WHISPERS_OF_DOOM.mission_name: MissionInfo(SC2Mission.WHISPERS_OF_DOOM, [MissionConnection(9, SC2Campaign.WOL)], SC2Mission.WHISPERS_OF_DOOM.area),
SC2Mission.A_SINISTER_TURN.mission_name: MissionInfo(SC2Mission.A_SINISTER_TURN, [MissionConnection(1, SC2Campaign.PROPHECY)], SC2Mission.A_SINISTER_TURN.area),
SC2Mission.ECHOES_OF_THE_FUTURE.mission_name: MissionInfo(SC2Mission.ECHOES_OF_THE_FUTURE, [MissionConnection(2, SC2Campaign.PROPHECY)], SC2Mission.ECHOES_OF_THE_FUTURE.area),
SC2Mission.IN_UTTER_DARKNESS.mission_name: MissionInfo(SC2Mission.IN_UTTER_DARKNESS, [MissionConnection(3, SC2Campaign.PROPHECY)], SC2Mission.IN_UTTER_DARKNESS.area)
},
SC2Campaign.HOTS: {
SC2Mission.LAB_RAT.mission_name: MissionInfo(SC2Mission.LAB_RAT, [], SC2Mission.LAB_RAT.area, completion_critical=True),
SC2Mission.BACK_IN_THE_SADDLE.mission_name: MissionInfo(SC2Mission.BACK_IN_THE_SADDLE, [MissionConnection(1, SC2Campaign.HOTS)], SC2Mission.BACK_IN_THE_SADDLE.area, completion_critical=True),
SC2Mission.RENDEZVOUS.mission_name: MissionInfo(SC2Mission.RENDEZVOUS, [MissionConnection(2, SC2Campaign.HOTS)], SC2Mission.RENDEZVOUS.area, completion_critical=True),
SC2Mission.HARVEST_OF_SCREAMS.mission_name: MissionInfo(SC2Mission.HARVEST_OF_SCREAMS, [MissionConnection(3, SC2Campaign.HOTS)], SC2Mission.HARVEST_OF_SCREAMS.area),
SC2Mission.SHOOT_THE_MESSENGER.mission_name: MissionInfo(SC2Mission.SHOOT_THE_MESSENGER, [MissionConnection(4, SC2Campaign.HOTS)], SC2Mission.SHOOT_THE_MESSENGER.area),
SC2Mission.ENEMY_WITHIN.mission_name: MissionInfo(SC2Mission.ENEMY_WITHIN, [MissionConnection(5, SC2Campaign.HOTS)], SC2Mission.ENEMY_WITHIN.area),
SC2Mission.DOMINATION.mission_name: MissionInfo(SC2Mission.DOMINATION, [MissionConnection(3, SC2Campaign.HOTS)], SC2Mission.DOMINATION.area),
SC2Mission.FIRE_IN_THE_SKY.mission_name: MissionInfo(SC2Mission.FIRE_IN_THE_SKY, [MissionConnection(7, SC2Campaign.HOTS)], SC2Mission.FIRE_IN_THE_SKY.area),
SC2Mission.OLD_SOLDIERS.mission_name: MissionInfo(SC2Mission.OLD_SOLDIERS, [MissionConnection(8, SC2Campaign.HOTS)], SC2Mission.OLD_SOLDIERS.area),
SC2Mission.WAKING_THE_ANCIENT.mission_name: MissionInfo(SC2Mission.WAKING_THE_ANCIENT, [MissionConnection(6, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.HOTS)], SC2Mission.WAKING_THE_ANCIENT.area, completion_critical=True, or_requirements=True),
SC2Mission.THE_CRUCIBLE.mission_name: MissionInfo(SC2Mission.THE_CRUCIBLE, [MissionConnection(10, SC2Campaign.HOTS)], SC2Mission.THE_CRUCIBLE.area, completion_critical=True),
SC2Mission.SUPREME.mission_name: MissionInfo(SC2Mission.SUPREME, [MissionConnection(11, SC2Campaign.HOTS)], SC2Mission.SUPREME.area, completion_critical=True),
SC2Mission.INFESTED.mission_name: MissionInfo(SC2Mission.INFESTED, [MissionConnection(6, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.HOTS), MissionConnection(12, SC2Campaign.HOTS)], SC2Mission.INFESTED.area),
SC2Mission.HAND_OF_DARKNESS.mission_name: MissionInfo(SC2Mission.HAND_OF_DARKNESS, [MissionConnection(13, SC2Campaign.HOTS)], SC2Mission.HAND_OF_DARKNESS.area),
SC2Mission.PHANTOMS_OF_THE_VOID.mission_name: MissionInfo(SC2Mission.PHANTOMS_OF_THE_VOID, [MissionConnection(14, SC2Campaign.HOTS)], SC2Mission.PHANTOMS_OF_THE_VOID.area),
SC2Mission.WITH_FRIENDS_LIKE_THESE.mission_name: MissionInfo(SC2Mission.WITH_FRIENDS_LIKE_THESE, [MissionConnection(6, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.HOTS), MissionConnection(12, SC2Campaign.HOTS)], SC2Mission.WITH_FRIENDS_LIKE_THESE.area),
SC2Mission.CONVICTION.mission_name: MissionInfo(SC2Mission.CONVICTION, [MissionConnection(16, SC2Campaign.HOTS)], SC2Mission.CONVICTION.area),
SC2Mission.PLANETFALL.mission_name: MissionInfo(SC2Mission.PLANETFALL, [MissionConnection(15, SC2Campaign.HOTS), MissionConnection(17, SC2Campaign.HOTS)], SC2Mission.PLANETFALL.area, completion_critical=True),
SC2Mission.DEATH_FROM_ABOVE.mission_name: MissionInfo(SC2Mission.DEATH_FROM_ABOVE, [MissionConnection(18, SC2Campaign.HOTS)], SC2Mission.DEATH_FROM_ABOVE.area, completion_critical=True),
SC2Mission.THE_RECKONING.mission_name: MissionInfo(SC2Mission.THE_RECKONING, [MissionConnection(19, SC2Campaign.HOTS)], SC2Mission.THE_RECKONING.area, completion_critical=True),
},
SC2Campaign.PROLOGUE: {
SC2Mission.DARK_WHISPERS.mission_name: MissionInfo(SC2Mission.DARK_WHISPERS, [], SC2Mission.DARK_WHISPERS.area),
SC2Mission.GHOSTS_IN_THE_FOG.mission_name: MissionInfo(SC2Mission.GHOSTS_IN_THE_FOG, [MissionConnection(1, SC2Campaign.PROLOGUE)], SC2Mission.GHOSTS_IN_THE_FOG.area),
SC2Mission.EVIL_AWOKEN.mission_name: MissionInfo(SC2Mission.EVIL_AWOKEN, [MissionConnection(2, SC2Campaign.PROLOGUE)], SC2Mission.EVIL_AWOKEN.area)
},
SC2Campaign.LOTV: {
SC2Mission.FOR_AIUR.mission_name: MissionInfo(SC2Mission.FOR_AIUR, [], SC2Mission.FOR_AIUR.area, completion_critical=True),
SC2Mission.THE_GROWING_SHADOW.mission_name: MissionInfo(SC2Mission.THE_GROWING_SHADOW, [MissionConnection(1, SC2Campaign.LOTV)], SC2Mission.THE_GROWING_SHADOW.area, completion_critical=True),
SC2Mission.THE_SPEAR_OF_ADUN.mission_name: MissionInfo(SC2Mission.THE_SPEAR_OF_ADUN, [MissionConnection(2, SC2Campaign.LOTV)], SC2Mission.THE_SPEAR_OF_ADUN.area, completion_critical=True),
SC2Mission.SKY_SHIELD.mission_name: MissionInfo(SC2Mission.SKY_SHIELD, [MissionConnection(3, SC2Campaign.LOTV)], SC2Mission.SKY_SHIELD.area, completion_critical=True),
SC2Mission.BROTHERS_IN_ARMS.mission_name: MissionInfo(SC2Mission.BROTHERS_IN_ARMS, [MissionConnection(4, SC2Campaign.LOTV)], SC2Mission.BROTHERS_IN_ARMS.area, completion_critical=True),
SC2Mission.AMON_S_REACH.mission_name: MissionInfo(SC2Mission.AMON_S_REACH, [MissionConnection(3, SC2Campaign.LOTV)], SC2Mission.AMON_S_REACH.area, completion_critical=True),
SC2Mission.LAST_STAND.mission_name: MissionInfo(SC2Mission.LAST_STAND, [MissionConnection(6, SC2Campaign.LOTV)], SC2Mission.LAST_STAND.area, completion_critical=True),
SC2Mission.FORBIDDEN_WEAPON.mission_name: MissionInfo(SC2Mission.FORBIDDEN_WEAPON, [MissionConnection(5, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV)], SC2Mission.FORBIDDEN_WEAPON.area, completion_critical=True, or_requirements=True),
SC2Mission.TEMPLE_OF_UNIFICATION.mission_name: MissionInfo(SC2Mission.TEMPLE_OF_UNIFICATION, [MissionConnection(5, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV), MissionConnection(8, SC2Campaign.LOTV)], SC2Mission.TEMPLE_OF_UNIFICATION.area, completion_critical=True),
SC2Mission.THE_INFINITE_CYCLE.mission_name: MissionInfo(SC2Mission.THE_INFINITE_CYCLE, [MissionConnection(9, SC2Campaign.LOTV)], SC2Mission.THE_INFINITE_CYCLE.area, completion_critical=True),
SC2Mission.HARBINGER_OF_OBLIVION.mission_name: MissionInfo(SC2Mission.HARBINGER_OF_OBLIVION, [MissionConnection(10, SC2Campaign.LOTV)], SC2Mission.HARBINGER_OF_OBLIVION.area, completion_critical=True),
SC2Mission.UNSEALING_THE_PAST.mission_name: MissionInfo(SC2Mission.UNSEALING_THE_PAST, [MissionConnection(11, SC2Campaign.LOTV)], SC2Mission.UNSEALING_THE_PAST.area, completion_critical=True),
SC2Mission.PURIFICATION.mission_name: MissionInfo(SC2Mission.PURIFICATION, [MissionConnection(12, SC2Campaign.LOTV)], SC2Mission.PURIFICATION.area, completion_critical=True),
SC2Mission.STEPS_OF_THE_RITE.mission_name: MissionInfo(SC2Mission.STEPS_OF_THE_RITE, [MissionConnection(11, SC2Campaign.LOTV)], SC2Mission.STEPS_OF_THE_RITE.area, completion_critical=True),
SC2Mission.RAK_SHIR.mission_name: MissionInfo(SC2Mission.RAK_SHIR, [MissionConnection(14, SC2Campaign.LOTV)], SC2Mission.RAK_SHIR.area, completion_critical=True),
SC2Mission.TEMPLAR_S_CHARGE.mission_name: MissionInfo(SC2Mission.TEMPLAR_S_CHARGE, [MissionConnection(13, SC2Campaign.LOTV), MissionConnection(15, SC2Campaign.LOTV)], SC2Mission.TEMPLAR_S_CHARGE.area, completion_critical=True, or_requirements=True),
SC2Mission.TEMPLAR_S_RETURN.mission_name: MissionInfo(SC2Mission.TEMPLAR_S_RETURN, [MissionConnection(13, SC2Campaign.LOTV), MissionConnection(15, SC2Campaign.LOTV), MissionConnection(16, SC2Campaign.LOTV)], SC2Mission.TEMPLAR_S_RETURN.area, completion_critical=True),
SC2Mission.THE_HOST.mission_name: MissionInfo(SC2Mission.THE_HOST, [MissionConnection(17, SC2Campaign.LOTV)], SC2Mission.THE_HOST.area, completion_critical=True),
SC2Mission.SALVATION.mission_name: MissionInfo(SC2Mission.SALVATION, [MissionConnection(18, SC2Campaign.LOTV)], SC2Mission.SALVATION.area, completion_critical=True),
},
SC2Campaign.EPILOGUE: {
SC2Mission.INTO_THE_VOID.mission_name: MissionInfo(SC2Mission.INTO_THE_VOID, [MissionConnection(25, SC2Campaign.WOL), MissionConnection(20, SC2Campaign.HOTS), MissionConnection(19, SC2Campaign.LOTV)], SC2Mission.INTO_THE_VOID.area, completion_critical=True),
SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name: MissionInfo(SC2Mission.THE_ESSENCE_OF_ETERNITY, [MissionConnection(1, SC2Campaign.EPILOGUE)], SC2Mission.THE_ESSENCE_OF_ETERNITY.area, completion_critical=True),
SC2Mission.AMON_S_FALL.mission_name: MissionInfo(SC2Mission.AMON_S_FALL, [MissionConnection(2, SC2Campaign.EPILOGUE)], SC2Mission.AMON_S_FALL.area, completion_critical=True),
},
SC2Campaign.NCO: {
SC2Mission.THE_ESCAPE.mission_name: MissionInfo(SC2Mission.THE_ESCAPE, [], SC2Mission.THE_ESCAPE.area, completion_critical=True),
SC2Mission.SUDDEN_STRIKE.mission_name: MissionInfo(SC2Mission.SUDDEN_STRIKE, [MissionConnection(1, SC2Campaign.NCO)], SC2Mission.SUDDEN_STRIKE.area, completion_critical=True),
SC2Mission.ENEMY_INTELLIGENCE.mission_name: MissionInfo(SC2Mission.ENEMY_INTELLIGENCE, [MissionConnection(2, SC2Campaign.NCO)], SC2Mission.ENEMY_INTELLIGENCE.area, completion_critical=True),
SC2Mission.TROUBLE_IN_PARADISE.mission_name: MissionInfo(SC2Mission.TROUBLE_IN_PARADISE, [MissionConnection(3, SC2Campaign.NCO)], SC2Mission.TROUBLE_IN_PARADISE.area, completion_critical=True),
SC2Mission.NIGHT_TERRORS.mission_name: MissionInfo(SC2Mission.NIGHT_TERRORS, [MissionConnection(4, SC2Campaign.NCO)], SC2Mission.NIGHT_TERRORS.area, completion_critical=True),
SC2Mission.FLASHPOINT.mission_name: MissionInfo(SC2Mission.FLASHPOINT, [MissionConnection(5, SC2Campaign.NCO)], SC2Mission.FLASHPOINT.area, completion_critical=True),
SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name: MissionInfo(SC2Mission.IN_THE_ENEMY_S_SHADOW, [MissionConnection(6, SC2Campaign.NCO)], SC2Mission.IN_THE_ENEMY_S_SHADOW.area, completion_critical=True),
SC2Mission.DARK_SKIES.mission_name: MissionInfo(SC2Mission.DARK_SKIES, [MissionConnection(7, SC2Campaign.NCO)], SC2Mission.DARK_SKIES.area, completion_critical=True),
SC2Mission.END_GAME.mission_name: MissionInfo(SC2Mission.END_GAME, [MissionConnection(8, SC2Campaign.NCO)], SC2Mission.END_GAME.area, completion_critical=True),
}
}
lookup_id_to_mission: Dict[int, SC2Mission] = {
mission.id: mission for mission in SC2Mission
}
lookup_name_to_mission: Dict[str, SC2Mission] = {
mission.mission_name: mission for mission in SC2Mission
}
lookup_id_to_campaign: Dict[int, SC2Campaign] = {
campaign.id: campaign for campaign in SC2Campaign
}
campaign_mission_table: Dict[SC2Campaign, Set[SC2Mission]] = {
campaign: set() for campaign in SC2Campaign
}
for mission in SC2Mission:
campaign_mission_table[mission.campaign].add(mission)
def get_campaign_difficulty(campaign: SC2Campaign, excluded_missions: Iterable[SC2Mission] = ()) -> MissionPools:
"""
:param campaign:
:param excluded_missions:
:return: Campaign's the most difficult non-excluded mission
"""
excluded_mission_set = set(excluded_missions)
included_missions = campaign_mission_table[campaign].difference(excluded_mission_set)
return max([mission.pool for mission in included_missions])
def get_campaign_goal_priority(campaign: SC2Campaign, excluded_missions: Iterable[SC2Mission] = ()) -> SC2CampaignGoalPriority:
"""
Gets a modified campaign goal priority.
If all the campaign's goal missions are excluded, it's ineligible to have the goal
If the campaign's very hard missions are excluded, the priority is lowered to hard
:param campaign:
:param excluded_missions:
:return:
"""
if excluded_missions is None:
return campaign.goal_priority
else:
goal_missions = set(get_campaign_potential_goal_missions(campaign))
excluded_mission_set = set(excluded_missions)
remaining_goals = goal_missions.difference(excluded_mission_set)
if remaining_goals == set():
# All potential goals are excluded, the campaign can't be a goal
return SC2CampaignGoalPriority.NONE
elif campaign.goal_priority == SC2CampaignGoalPriority.VERY_HARD:
# Check if a very hard campaign doesn't get rid of it's last very hard mission
difficulty = get_campaign_difficulty(campaign, excluded_missions)
if difficulty == MissionPools.VERY_HARD:
return SC2CampaignGoalPriority.VERY_HARD
else:
return SC2CampaignGoalPriority.HARD
else:
return campaign.goal_priority
class SC2CampaignGoal(NamedTuple):
mission: SC2Mission
location: str
campaign_final_mission_locations: Dict[SC2Campaign, SC2CampaignGoal] = {
SC2Campaign.WOL: SC2CampaignGoal(SC2Mission.ALL_IN, "All-In: Victory"),
SC2Campaign.PROPHECY: SC2CampaignGoal(SC2Mission.IN_UTTER_DARKNESS, "In Utter Darkness: Kills"),
SC2Campaign.HOTS: None,
SC2Campaign.PROLOGUE: SC2CampaignGoal(SC2Mission.EVIL_AWOKEN, "Evil Awoken: Victory"),
SC2Campaign.LOTV: SC2CampaignGoal(SC2Mission.SALVATION, "Salvation: Victory"),
SC2Campaign.EPILOGUE: None,
SC2Campaign.NCO: None,
}
campaign_alt_final_mission_locations: Dict[SC2Campaign, Dict[SC2Mission, str]] = {
SC2Campaign.WOL: {
SC2Mission.MAW_OF_THE_VOID: "Maw of the Void: Victory",
SC2Mission.ENGINE_OF_DESTRUCTION: "Engine of Destruction: Victory",
SC2Mission.SUPERNOVA: "Supernova: Victory",
SC2Mission.GATES_OF_HELL: "Gates of Hell: Victory",
SC2Mission.SHATTER_THE_SKY: "Shatter the Sky: Victory"
},
SC2Campaign.PROPHECY: None,
SC2Campaign.HOTS: {
SC2Mission.THE_RECKONING: "The Reckoning: Victory",
SC2Mission.THE_CRUCIBLE: "The Crucible: Victory",
SC2Mission.HAND_OF_DARKNESS: "Hand of Darkness: Victory",
SC2Mission.PHANTOMS_OF_THE_VOID: "Phantoms of the Void: Victory",
SC2Mission.PLANETFALL: "Planetfall: Victory",
SC2Mission.DEATH_FROM_ABOVE: "Death From Above: Victory"
},
SC2Campaign.PROLOGUE: {
SC2Mission.GHOSTS_IN_THE_FOG: "Ghosts in the Fog: Victory"
},
SC2Campaign.LOTV: {
SC2Mission.THE_HOST: "The Host: Victory",
SC2Mission.TEMPLAR_S_CHARGE: "Templar's Charge: Victory"
},
SC2Campaign.EPILOGUE: {
SC2Mission.AMON_S_FALL: "Amon's Fall: Victory",
SC2Mission.INTO_THE_VOID: "Into the Void: Victory",
SC2Mission.THE_ESSENCE_OF_ETERNITY: "The Essence of Eternity: Victory",
},
SC2Campaign.NCO: {
SC2Mission.END_GAME: "End Game: Victory",
SC2Mission.FLASHPOINT: "Flashpoint: Victory",
SC2Mission.DARK_SKIES: "Dark Skies: Victory",
SC2Mission.NIGHT_TERRORS: "Night Terrors: Victory",
SC2Mission.TROUBLE_IN_PARADISE: "Trouble In Paradise: Victory"
}
}
campaign_race_exceptions: Dict[SC2Mission, SC2Race] = {
SC2Mission.WITH_FRIENDS_LIKE_THESE: SC2Race.TERRAN
}
def get_goal_location(mission: SC2Mission) -> Union[str, None]:
"""
:param mission:
:return: Goal location assigned to the goal mission
"""
campaign = mission.campaign
primary_campaign_goal = campaign_final_mission_locations[campaign]
if primary_campaign_goal is not None:
if primary_campaign_goal.mission == mission:
return primary_campaign_goal.location
campaign_alt_goals = campaign_alt_final_mission_locations[campaign]
if campaign_alt_goals is not None:
return campaign_alt_goals.get(mission)
return None
def get_campaign_potential_goal_missions(campaign: SC2Campaign) -> List[SC2Mission]:
"""
:param campaign:
:return: All missions that can be the campaign's goal
"""
missions: List[SC2Mission] = list()
primary_goal_mission = campaign_final_mission_locations[campaign]
if primary_goal_mission is not None:
missions.append(primary_goal_mission.mission)
alt_goal_locations = campaign_alt_final_mission_locations[campaign]
if alt_goal_locations is not None:
for mission in alt_goal_locations.keys():
missions.append(mission)
return missions
def get_no_build_missions() -> List[SC2Mission]:
return [mission for mission in SC2Mission if not mission.build]

908
worlds/sc2/Options.py Normal file
View File

@ -0,0 +1,908 @@
from dataclasses import dataclass, fields, Field
from typing import FrozenSet, Union, Set
from Options import Choice, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range, PerGameCommonOptions
from .MissionTables import SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_no_build_missions, \
campaign_mission_table
from worlds.AutoWorld import World
class GameDifficulty(Choice):
"""
The difficulty of the campaign, affects enemy AI, starting units, and game speed.
For those unfamiliar with the Archipelago randomizer, the recommended settings are one difficulty level
lower than the vanilla game
"""
display_name = "Game Difficulty"
option_casual = 0
option_normal = 1
option_hard = 2
option_brutal = 3
default = 1
class GameSpeed(Choice):
"""Optional setting to override difficulty-based game speed."""
display_name = "Game Speed"
option_default = 0
option_slower = 1
option_slow = 2
option_normal = 3
option_fast = 4
option_faster = 5
default = option_default
class DisableForcedCamera(Toggle):
"""
Prevents the game from moving or locking the camera without the player's consent.
"""
display_name = "Disable Forced Camera Movement"
class SkipCutscenes(Toggle):
"""
Skips all cutscenes and prevents dialog from blocking progress.
"""
display_name = "Skip Cutscenes"
class AllInMap(Choice):
"""Determines what version of All-In (WoL final map) that will be generated for the campaign."""
display_name = "All In Map"
option_ground = 0
option_air = 1
class MissionOrder(Choice):
"""
Determines the order the missions are played in. The last three mission orders end in a random mission.
Vanilla (83 total if all campaigns enabled): Keeps the standard mission order and branching from the vanilla Campaigns.
Vanilla Shuffled (83 total if all campaigns enabled): Keeps same branching paths from the vanilla Campaigns but randomizes the order of missions within.
Mini Campaign (47 total if all campaigns enabled): Shorter version of the campaign with randomized missions and optional branches.
Medium Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards bottom-right mission to win.
Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win.
Blitz (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win.
Gauntlet (7): Linear series of 7 random missions to complete the campaign.
Mini Gauntlet (4): Linear series of 4 random missions to complete the campaign.
Tiny Grid (4): A 2x2 version of Grid. Complete the bottom-right mission to win.
Grid (variable): A grid that will resize to use all non-excluded missions. Corners may be omitted to make the grid more square. Complete the bottom-right mission to win.
"""
display_name = "Mission Order"
option_vanilla = 0
option_vanilla_shuffled = 1
option_mini_campaign = 2
option_medium_grid = 3
option_mini_grid = 4
option_blitz = 5
option_gauntlet = 6
option_mini_gauntlet = 7
option_tiny_grid = 8
option_grid = 9
class MaximumCampaignSize(Range):
"""
Sets an upper bound on how many missions to include when a variable-size mission order is selected.
If a set-size mission order is selected, does nothing.
"""
display_name = "Maximum Campaign Size"
range_start = 1
range_end = 83
default = 83
class GridTwoStartPositions(Toggle):
"""
If turned on and 'grid' mission order is selected, removes a mission from the starting
corner sets the adjacent two missions as the starter missions.
"""
display_name = "Start with two unlocked missions on grid"
default = Toggle.option_false
class ColorChoice(Choice):
option_white = 0
option_red = 1
option_blue = 2
option_teal = 3
option_purple = 4
option_yellow = 5
option_orange = 6
option_green = 7
option_light_pink = 8
option_violet = 9
option_light_grey = 10
option_dark_green = 11
option_brown = 12
option_light_green = 13
option_dark_grey = 14
option_pink = 15
option_rainbow = 16
option_default = 17
default = option_default
class PlayerColorTerranRaynor(ColorChoice):
"""Determines in-game team color for playable Raynor's Raiders (Terran) factions."""
display_name = "Terran Player Color (Raynor)"
class PlayerColorProtoss(ColorChoice):
"""Determines in-game team color for playable Protoss factions."""
display_name = "Protoss Player Color"
class PlayerColorZerg(ColorChoice):
"""Determines in-game team color for playable Zerg factions before Kerrigan becomes Primal Kerrigan."""
display_name = "Zerg Player Color"
class PlayerColorZergPrimal(ColorChoice):
"""Determines in-game team color for playable Zerg factions after Kerrigan becomes Primal Kerrigan."""
display_name = "Zerg Player Color (Primal)"
class EnableWolMissions(DefaultOnToggle):
"""
Enables missions from main Wings of Liberty campaign.
"""
display_name = "Enable Wings of Liberty missions"
class EnableProphecyMissions(DefaultOnToggle):
"""
Enables missions from Prophecy mini-campaign.
"""
display_name = "Enable Prophecy missions"
class EnableHotsMissions(DefaultOnToggle):
"""
Enables missions from Heart of the Swarm campaign.
"""
display_name = "Enable Heart of the Swarm missions"
class EnableLotVPrologueMissions(DefaultOnToggle):
"""
Enables missions from Prologue campaign.
"""
display_name = "Enable Prologue (Legacy of the Void) missions"
class EnableLotVMissions(DefaultOnToggle):
"""
Enables missions from Legacy of the Void campaign.
"""
display_name = "Enable Legacy of the Void (main campaign) missions"
class EnableEpilogueMissions(DefaultOnToggle):
"""
Enables missions from Epilogue campaign.
These missions are considered very hard.
Enabling Wings of Liberty, Heart of the Swarm and Legacy of the Void is strongly recommended in order to play Epilogue.
Not recommended for short mission orders.
See also: Exclude Very Hard Missions
"""
display_name = "Enable Epilogue missions"
class EnableNCOMissions(DefaultOnToggle):
"""
Enables missions from Nova Covert Ops campaign.
Note: For best gameplay experience it's recommended to also enable Wings of Liberty campaign.
"""
display_name = "Enable Nova Covert Ops missions"
class ShuffleCampaigns(DefaultOnToggle):
"""
Shuffles the missions between campaigns if enabled.
Only available for Vanilla Shuffled and Mini Campaign mission order
"""
display_name = "Shuffle Campaigns"
class ShuffleNoBuild(DefaultOnToggle):
"""
Determines if the no-build missions are included in the shuffle.
If turned off, the no-build missions will not appear. Has no effect for Vanilla mission order.
"""
display_name = "Shuffle No-Build Missions"
class StarterUnit(Choice):
"""
Unlocks a random unit at the start of the game.
Off: No units are provided, the first unit must be obtained from the randomizer
Balanced: A unit that doesn't give the player too much power early on is given
Any Starter Unit: Any starter unit can be given
"""
display_name = "Starter Unit"
option_off = 0
option_balanced = 1
option_any_starter_unit = 2
class RequiredTactics(Choice):
"""
Determines the maximum tactical difficulty of the world (separate from mission difficulty). Higher settings
increase randomness.
Standard: All missions can be completed with good micro and macro.
Advanced: Completing missions may require relying on starting units and micro-heavy units.
No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!
Locks Grant Story Tech option to true.
"""
display_name = "Required Tactics"
option_standard = 0
option_advanced = 1
option_no_logic = 2
class GenericUpgradeMissions(Range):
"""Determines the percentage of missions in the mission order that must be completed before
level 1 of all weapon and armor upgrades is unlocked. Level 2 upgrades require double the amount of missions,
and level 3 requires triple the amount. The required amounts are always rounded down.
If set to 0, upgrades are instead added to the item pool and must be found to be used."""
display_name = "Generic Upgrade Missions"
range_start = 0
range_end = 100
default = 0
class GenericUpgradeResearch(Choice):
"""Determines how weapon and armor upgrades affect missions once unlocked.
Vanilla: Upgrades must be researched as normal.
Auto In No-Build: In No-Build missions, upgrades are automatically researched.
In all other missions, upgrades must be researched as normal.
Auto In Build: In No-Build missions, upgrades are unavailable as normal.
In all other missions, upgrades are automatically researched.
Always Auto: Upgrades are automatically researched in all missions."""
display_name = "Generic Upgrade Research"
option_vanilla = 0
option_auto_in_no_build = 1
option_auto_in_build = 2
option_always_auto = 3
class GenericUpgradeItems(Choice):
"""Determines how weapon and armor upgrades are split into items. All options produce 3 levels of each item.
Does nothing if upgrades are unlocked by completed mission counts.
Individual Items: All weapon and armor upgrades are each an item,
resulting in 18 total upgrade items for Terran and 15 total items for Zerg and Protoss each.
Bundle Weapon And Armor: All types of weapon upgrades are one item per race,
and all types of armor upgrades are one item per race,
resulting in 18 total items.
Bundle Unit Class: Weapon and armor upgrades are merged,
but upgrades are bundled separately for each race:
Infantry, Vehicle, and Starship upgrades for Terran (9 items),
Ground and Flyer upgrades for Zerg (6 items),
Ground and Air upgrades for Protoss (6 items),
resulting in 21 total items.
Bundle All: All weapon and armor upgrades are one item per race,
resulting in 9 total items."""
display_name = "Generic Upgrade Items"
option_individual_items = 0
option_bundle_weapon_and_armor = 1
option_bundle_unit_class = 2
option_bundle_all = 3
class NovaCovertOpsItems(Toggle):
"""
If turned on, the equipment upgrades from Nova Covert Ops may be present in the world.
If Nova Covert Ops campaign is enabled, this option is locked to be turned on.
"""
display_name = "Nova Covert Ops Items"
default = Toggle.option_true
class BroodWarItems(Toggle):
"""If turned on, returning items from StarCraft: Brood War may appear in the world."""
display_name = "Brood War Items"
default = Toggle.option_true
class ExtendedItems(Toggle):
"""If turned on, original items that did not appear in Campaign mode may appear in the world."""
display_name = "Extended Items"
default = Toggle.option_true
# Current maximum number of upgrades for a unit
MAX_UPGRADES_OPTION = 12
class EnsureGenericItems(Range):
"""
Specifies a minimum percentage of the generic item pool that will be present for the slot.
The generic item pool is the pool of all generically useful items after all exclusions.
Generically-useful items include: Worker upgrades, Building upgrades, economy upgrades,
Mercenaries, Kerrigan levels and abilities, and Spear of Adun abilities
Increasing this percentage will make units less common.
"""
display_name = "Ensure Generic Items"
range_start = 0
range_end = 100
default = 25
class MinNumberOfUpgrades(Range):
"""
Set a minimum to the number of upgrades a unit/structure can have.
Note that most units have 4 or 6 upgrades.
If a unit has fewer upgrades than the minimum, it will have all of its upgrades.
Doesn't affect shared unit upgrades.
"""
display_name = "Minimum number of upgrades per unit/structure"
range_start = 0
range_end = MAX_UPGRADES_OPTION
default = 2
class MaxNumberOfUpgrades(Range):
"""
Set a maximum to the number of upgrades a unit/structure can have. -1 is used to define unlimited.
Note that most unit have 4 to 6 upgrades.
Doesn't affect shared unit upgrades.
"""
display_name = "Maximum number of upgrades per unit/structure"
range_start = -1
range_end = MAX_UPGRADES_OPTION
default = -1
class KerriganPresence(Choice):
"""
Determines whether Kerrigan is playable outside of missions that require her.
Vanilla: Kerrigan is playable as normal, appears in the same missions as in vanilla game.
Not Present: Kerrigan is not playable, unless the mission requires her to be present. Other hero units stay playable,
and locations normally requiring Kerrigan can be checked by any unit.
Kerrigan level items, active abilities and passive abilities affecting her will not appear.
In missions where the Kerrigan unit is required, story abilities are given in same way as Grant Story Tech is set to true
Not Present And No Passives: In addition to the above, Kerrigan's passive abilities affecting other units (such as Twin Drones) will not appear.
Note: Always set to "Not Present" if Heart of the Swarm campaign is disabled.
"""
display_name = "Kerrigan Presence"
option_vanilla = 0
option_not_present = 1
option_not_present_and_no_passives = 2
class KerriganLevelsPerMissionCompleted(Range):
"""
Determines how many levels Kerrigan gains when a mission is beaten.
NOTE: Setting this too low can result in generation failures if The Infinite Cycle or Supreme are in the mission pool.
"""
display_name = "Levels Per Mission Beaten"
range_start = 0
range_end = 20
default = 0
class KerriganLevelsPerMissionCompletedCap(Range):
"""
Limits how many total levels Kerrigan can gain from beating missions. This does not affect levels gained from items.
Set to -1 to disable this limit.
NOTE: The following missions have these level requirements:
Supreme: 35
The Infinite Cycle: 70
See Grant Story Levels for more details.
"""
display_name = "Levels Per Mission Beaten Cap"
range_start = -1
range_end = 140
default = -1
class KerriganLevelItemSum(Range):
"""
Determines the sum of the level items in the world. This does not affect levels gained from beating missions.
NOTE: The following missions have these level requirements:
Supreme: 35
The Infinite Cycle: 70
See Grant Story Levels for more details.
"""
display_name = "Kerrigan Level Item Sum"
range_start = 0
range_end = 140
default = 70
class KerriganLevelItemDistribution(Choice):
"""Determines the amount and size of Kerrigan level items.
Vanilla: Uses the distribution in the vanilla campaign.
This entails 32 individual levels and 6 packs of varying sizes.
This distribution always adds up to 70, ignoring the Level Item Sum setting.
Smooth: Uses a custom, condensed distribution of 10 items between sizes 4 and 10,
intended to fit more levels into settings with little room for filler while keeping some variance in level gains.
This distribution always adds up to 70, ignoring the Level Item Sum setting.
Size 70: Uses items worth 70 levels each.
Size 35: Uses items worth 35 levels each.
Size 14: Uses items worth 14 levels each.
Size 10: Uses items worth 10 levels each.
Size 7: Uses items worth 7 levels each.
Size 5: Uses items worth 5 levels each.
Size 2: Uses items worth 2 level eachs.
Size 1: Uses individual levels. As there are not enough locations in the game for this distribution,
this will result in a greatly reduced total level, and is likely to remove many other items."""
display_name = "Kerrigan Level Item Distribution"
option_vanilla = 0
option_smooth = 1
option_size_70 = 2
option_size_35 = 3
option_size_14 = 4
option_size_10 = 5
option_size_7 = 6
option_size_5 = 7
option_size_2 = 8
option_size_1 = 9
default = option_smooth
class KerriganTotalLevelCap(Range):
"""
Limits how many total levels Kerrigan can gain from any source. Depending on your other settings,
there may be more levels available in the world, but they will not affect Kerrigan.
Set to -1 to disable this limit.
NOTE: The following missions have these level requirements:
Supreme: 35
The Infinite Cycle: 70
See Grant Story Levels for more details.
"""
display_name = "Total Level Cap"
range_start = -1
range_end = 140
default = -1
class StartPrimaryAbilities(Range):
"""Number of Primary Abilities (Kerrigan Tier 1, 2, and 4) to start the game with.
If set to 4, a Tier 7 ability is also included."""
display_name = "Starting Primary Abilities"
range_start = 0
range_end = 4
default = 0
class KerriganPrimalStatus(Choice):
"""Determines when Kerrigan appears in her Primal Zerg form.
This greatly increases her energy regeneration.
Vanilla: Kerrigan is human in missions that canonically appear before The Crucible,
and zerg thereafter.
Always Zerg: Kerrigan is always zerg.
Always Human: Kerrigan is always human.
Level 35: Kerrigan is human until reaching level 35, and zerg thereafter.
Half Completion: Kerrigan is human until half of the missions in the world are completed,
and zerg thereafter.
Item: Kerrigan's Primal Form is an item. She is human until it is found, and zerg thereafter."""
display_name = "Kerrigan Primal Status"
option_vanilla = 0
option_always_zerg = 1
option_always_human = 2
option_level_35 = 3
option_half_completion = 4
option_item = 5
class SpearOfAdunPresence(Choice):
"""
Determines in which missions Spear of Adun calldowns will be available.
Affects only abilities used from Spear of Adun top menu.
Not Present: Spear of Adun calldowns are unavailable.
LotV Protoss: Spear of Adun calldowns are only available in LotV main campaign
Protoss: Spear od Adun calldowns are available in any Protoss mission
Everywhere: Spear od Adun calldowns are available in any mission of any race
"""
display_name = "Spear of Adun Presence"
option_not_present = 0
option_lotv_protoss = 1
option_protoss = 2
option_everywhere = 3
default = option_lotv_protoss
# Fix case
@classmethod
def get_option_name(cls, value: int) -> str:
if value == SpearOfAdunPresence.option_lotv_protoss:
return "LotV Protoss"
else:
return super().get_option_name(value)
class SpearOfAdunPresentInNoBuild(Toggle):
"""
Determines if Spear of Adun calldowns are available in no-build missions.
If turned on, Spear of Adun calldown powers are available in missions specified under "Spear of Adun Presence".
If turned off, Spear of Adun calldown powers are unavailable in all no-build missions
"""
display_name = "Spear of Adun Present in No-Build"
class SpearOfAdunAutonomouslyCastAbilityPresence(Choice):
"""
Determines availability of Spear of Adun powers, that are autonomously cast.
Affects abilities like Reconstruction Beam or Overwatch
Not Presents: Autocasts are not available.
LotV Protoss: Spear of Adun autocasts are only available in LotV main campaign
Protoss: Spear od Adun autocasts are available in any Protoss mission
Everywhere: Spear od Adun autocasts are available in any mission of any race
"""
display_name = "Spear of Adun Autonomously Cast Powers Presence"
option_not_present = 0
option_lotv_protoss = 1
option_protoss = 2
option_everywhere = 3
default = option_lotv_protoss
# Fix case
@classmethod
def get_option_name(cls, value: int) -> str:
if value == SpearOfAdunPresence.option_lotv_protoss:
return "LotV Protoss"
else:
return super().get_option_name(value)
class SpearOfAdunAutonomouslyCastPresentInNoBuild(Toggle):
"""
Determines if Spear of Adun autocasts are available in no-build missions.
If turned on, Spear of Adun autocasts are available in missions specified under "Spear of Adun Autonomously Cast Powers Presence".
If turned off, Spear of Adun autocasts are unavailable in all no-build missions
"""
display_name = "Spear of Adun Autonomously Cast Powers Present in No-Build"
class GrantStoryTech(Toggle):
"""
If set true, grants special tech required for story mission completion for duration of the mission.
Otherwise, you need to find these tech by a normal means as items.
Affects story missions like Back in the Saddle and Supreme
Locked to true if Required Tactics is set to no logic.
"""
display_name = "Grant Story Tech"
class GrantStoryLevels(Choice):
"""
If enabled, grants Kerrigan the required minimum levels for the following missions:
Supreme: 35
The Infinite Cycle: 70
The bonus levels only apply during the listed missions, and can exceed the Total Level Cap.
If disabled, either of these missions is included, and there are not enough levels in the world, generation may fail.
To prevent this, either increase the amount of levels in the world, or enable this option.
If disabled and Required Tactics is set to no logic, this option is forced to Minimum.
Disabled: Kerrigan does not get bonus levels for these missions,
instead the levels must be gained from items or beating missions.
Additive: Kerrigan gains bonus levels equal to the mission's required level.
Minimum: Kerrigan is either at her real level, or at the mission's required level,
depending on which is higher.
"""
display_name = "Grant Story Levels"
option_disabled = 0
option_additive = 1
option_minimum = 2
default = option_minimum
class TakeOverAIAllies(Toggle):
"""
On maps supporting this feature allows you to take control over an AI Ally.
"""
display_name = "Take Over AI Allies"
class LockedItems(ItemSet):
"""Guarantees that these items will be unlockable"""
display_name = "Locked Items"
class ExcludedItems(ItemSet):
"""Guarantees that these items will not be unlockable"""
display_name = "Excluded Items"
class ExcludedMissions(OptionSet):
"""Guarantees that these missions will not appear in the campaign
Doesn't apply to vanilla mission order.
It may be impossible to build a valid campaign if too many missions are excluded."""
display_name = "Excluded Missions"
valid_keys = {mission.mission_name for mission in SC2Mission}
class ExcludeVeryHardMissions(Choice):
"""
Excludes Very Hard missions outside of Epilogue campaign (All-In, Salvation, and all Epilogue missions are considered Very Hard).
Doesn't apply to "Vanilla" mission order.
Default: Not excluded for mission orders "Vanilla Shuffled" or "Grid" with Maximum Campaign Size >= 20,
excluded for any other order
Yes: Non-Epilogue Very Hard missions are excluded and won't be generated
No: Non-Epilogue Very Hard missions can appear normally. Not recommended for too short mission orders.
See also: Excluded Missions, Enable Epilogue Missions, Maximum Campaign Size
"""
display_name = "Exclude Very Hard Missions"
option_default = 0
option_true = 1
option_false = 2
@classmethod
def get_option_name(cls, value):
return ["Default", "Yes", "No"][int(value)]
class LocationInclusion(Choice):
option_enabled = 0
option_resources = 1
option_disabled = 2
class VanillaLocations(LocationInclusion):
"""
Enables or disables item rewards for completing vanilla objectives.
Vanilla objectives are bonus objectives from the vanilla game,
along with some additional objectives to balance the missions.
Enable these locations for a balanced experience.
Enabled: All locations fitting into this do their normal rewards
Resources: Forces these locations to contain Starting Resources
Disabled: Removes item rewards from these locations.
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Vanilla Locations"
class ExtraLocations(LocationInclusion):
"""
Enables or disables item rewards for mission progress and minor objectives.
This includes mandatory mission objectives,
collecting reinforcements and resource pickups,
destroying structures, and overcoming minor challenges.
Enables these locations to add more checks and items to your world.
Enabled: All locations fitting into this do their normal rewards
Resources: Forces these locations to contain Starting Resources
Disabled: Removes item rewards from these locations.
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Extra Locations"
class ChallengeLocations(LocationInclusion):
"""
Enables or disables item rewards for completing challenge tasks.
Challenges are tasks that are more difficult than completing the mission, and are often based on achievements.
You might be required to visit the same mission later after getting stronger in order to finish these tasks.
Enable these locations to increase the difficulty of completing the multiworld.
Enabled: All locations fitting into this do their normal rewards
Resources: Forces these locations to contain Starting Resources
Disabled: Removes item rewards from these locations.
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Challenge Locations"
class MasteryLocations(LocationInclusion):
"""
Enables or disables item rewards for overcoming especially difficult challenges.
These challenges are often based on Mastery achievements and Feats of Strength.
Enable these locations to add the most difficult checks to the world.
Enabled: All locations fitting into this do their normal rewards
Resources: Forces these locations to contain Starting Resources
Disabled: Removes item rewards from these locations.
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Mastery Locations"
class MineralsPerItem(Range):
"""
Configures how many minerals are given per resource item.
"""
display_name = "Minerals Per Item"
range_start = 0
range_end = 500
default = 25
class VespenePerItem(Range):
"""
Configures how much vespene gas is given per resource item.
"""
display_name = "Vespene Per Item"
range_start = 0
range_end = 500
default = 25
class StartingSupplyPerItem(Range):
"""
Configures how much starting supply per is given per item.
"""
display_name = "Starting Supply Per Item"
range_start = 0
range_end = 200
default = 5
@dataclass
class Starcraft2Options(PerGameCommonOptions):
game_difficulty: GameDifficulty
game_speed: GameSpeed
disable_forced_camera: DisableForcedCamera
skip_cutscenes: SkipCutscenes
all_in_map: AllInMap
mission_order: MissionOrder
maximum_campaign_size: MaximumCampaignSize
grid_two_start_positions: GridTwoStartPositions
player_color_terran_raynor: PlayerColorTerranRaynor
player_color_protoss: PlayerColorProtoss
player_color_zerg: PlayerColorZerg
player_color_zerg_primal: PlayerColorZergPrimal
enable_wol_missions: EnableWolMissions
enable_prophecy_missions: EnableProphecyMissions
enable_hots_missions: EnableHotsMissions
enable_lotv_prologue_missions: EnableLotVPrologueMissions
enable_lotv_missions: EnableLotVMissions
enable_epilogue_missions: EnableEpilogueMissions
enable_nco_missions: EnableNCOMissions
shuffle_campaigns: ShuffleCampaigns
shuffle_no_build: ShuffleNoBuild
starter_unit: StarterUnit
required_tactics: RequiredTactics
ensure_generic_items: EnsureGenericItems
min_number_of_upgrades: MinNumberOfUpgrades
max_number_of_upgrades: MaxNumberOfUpgrades
generic_upgrade_missions: GenericUpgradeMissions
generic_upgrade_research: GenericUpgradeResearch
generic_upgrade_items: GenericUpgradeItems
kerrigan_presence: KerriganPresence
kerrigan_levels_per_mission_completed: KerriganLevelsPerMissionCompleted
kerrigan_levels_per_mission_completed_cap: KerriganLevelsPerMissionCompletedCap
kerrigan_level_item_sum: KerriganLevelItemSum
kerrigan_level_item_distribution: KerriganLevelItemDistribution
kerrigan_total_level_cap: KerriganTotalLevelCap
start_primary_abilities: StartPrimaryAbilities
kerrigan_primal_status: KerriganPrimalStatus
spear_of_adun_presence: SpearOfAdunPresence
spear_of_adun_present_in_no_build: SpearOfAdunPresentInNoBuild
spear_of_adun_autonomously_cast_ability_presence: SpearOfAdunAutonomouslyCastAbilityPresence
spear_of_adun_autonomously_cast_present_in_no_build: SpearOfAdunAutonomouslyCastPresentInNoBuild
grant_story_tech: GrantStoryTech
grant_story_levels: GrantStoryLevels
take_over_ai_allies: TakeOverAIAllies
locked_items: LockedItems
excluded_items: ExcludedItems
excluded_missions: ExcludedMissions
exclude_very_hard_missions: ExcludeVeryHardMissions
nco_items: NovaCovertOpsItems
bw_items: BroodWarItems
ext_items: ExtendedItems
vanilla_locations: VanillaLocations
extra_locations: ExtraLocations
challenge_locations: ChallengeLocations
mastery_locations: MasteryLocations
minerals_per_item: MineralsPerItem
vespene_per_item: VespenePerItem
starting_supply_per_item: StartingSupplyPerItem
def get_option_value(world: World, name: str) -> Union[int, FrozenSet]:
if world is None:
field: Field = [class_field for class_field in fields(Starcraft2Options) if class_field.name == name][0]
return field.type.default
player_option = getattr(world.options, name)
return player_option.value
def get_enabled_campaigns(world: World) -> Set[SC2Campaign]:
enabled_campaigns = set()
if get_option_value(world, "enable_wol_missions"):
enabled_campaigns.add(SC2Campaign.WOL)
if get_option_value(world, "enable_prophecy_missions"):
enabled_campaigns.add(SC2Campaign.PROPHECY)
if get_option_value(world, "enable_hots_missions"):
enabled_campaigns.add(SC2Campaign.HOTS)
if get_option_value(world, "enable_lotv_prologue_missions"):
enabled_campaigns.add(SC2Campaign.PROLOGUE)
if get_option_value(world, "enable_lotv_missions"):
enabled_campaigns.add(SC2Campaign.LOTV)
if get_option_value(world, "enable_epilogue_missions"):
enabled_campaigns.add(SC2Campaign.EPILOGUE)
if get_option_value(world, "enable_nco_missions"):
enabled_campaigns.add(SC2Campaign.NCO)
return enabled_campaigns
def get_disabled_campaigns(world: World) -> Set[SC2Campaign]:
all_campaigns = set(SC2Campaign)
enabled_campaigns = get_enabled_campaigns(world)
disabled_campaigns = all_campaigns.difference(enabled_campaigns)
disabled_campaigns.remove(SC2Campaign.GLOBAL)
return disabled_campaigns
def get_excluded_missions(world: World) -> Set[SC2Mission]:
mission_order_type = get_option_value(world, "mission_order")
excluded_mission_names = get_option_value(world, "excluded_missions")
shuffle_no_build = get_option_value(world, "shuffle_no_build")
disabled_campaigns = get_disabled_campaigns(world)
excluded_missions: Set[SC2Mission] = set([lookup_name_to_mission[name] for name in excluded_mission_names])
# Excluding Very Hard missions depending on options
if (get_option_value(world, "exclude_very_hard_missions") == ExcludeVeryHardMissions.option_true
) or (
get_option_value(world, "exclude_very_hard_missions") == ExcludeVeryHardMissions.option_default
and (
mission_order_type not in [MissionOrder.option_vanilla_shuffled, MissionOrder.option_grid]
or (
mission_order_type == MissionOrder.option_grid
and get_option_value(world, "maximum_campaign_size") < 20
)
)
):
excluded_missions = excluded_missions.union(
[mission for mission in SC2Mission if
mission.pool == MissionPools.VERY_HARD and mission.campaign != SC2Campaign.EPILOGUE]
)
# Omitting No-Build missions if not shuffling no-build
if not shuffle_no_build:
excluded_missions = excluded_missions.union(get_no_build_missions())
# Omitting missions not in enabled campaigns
for campaign in disabled_campaigns:
excluded_missions = excluded_missions.union(campaign_mission_table[campaign])
return excluded_missions
campaign_depending_orders = [
MissionOrder.option_vanilla,
MissionOrder.option_vanilla_shuffled,
MissionOrder.option_mini_campaign
]
kerrigan_unit_available = [
KerriganPresence.option_vanilla,
]

595
worlds/sc2/PoolFilter.py Normal file
View File

@ -0,0 +1,595 @@
from typing import Callable, Dict, List, Set, Union, Tuple
from BaseClasses import Item, Location
from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, progressive_if_nco, \
progressive_if_ext, spear_of_adun_calldowns, spear_of_adun_castable_passives, nova_equipment
from .MissionTables import mission_orders, MissionInfo, MissionPools, \
get_campaign_goal_priority, campaign_final_mission_locations, campaign_alt_final_mission_locations, \
SC2Campaign, SC2Race, SC2CampaignGoalPriority, SC2Mission
from .Options import get_option_value, MissionOrder, \
get_enabled_campaigns, get_disabled_campaigns, RequiredTactics, kerrigan_unit_available, GrantStoryTech, \
TakeOverAIAllies, SpearOfAdunPresence, SpearOfAdunAutonomouslyCastAbilityPresence, campaign_depending_orders, \
ShuffleCampaigns, get_excluded_missions, ShuffleNoBuild, ExtraLocations, GrantStoryLevels
from . import ItemNames
from worlds.AutoWorld import World
# Items with associated upgrades
UPGRADABLE_ITEMS = {item.parent_item for item in get_full_item_list().values() if item.parent_item}
BARRACKS_UNITS = {
ItemNames.MARINE, ItemNames.MEDIC, ItemNames.FIREBAT, ItemNames.MARAUDER,
ItemNames.REAPER, ItemNames.GHOST, ItemNames.SPECTRE, ItemNames.HERC,
}
FACTORY_UNITS = {
ItemNames.HELLION, ItemNames.VULTURE, ItemNames.GOLIATH, ItemNames.DIAMONDBACK,
ItemNames.SIEGE_TANK, ItemNames.THOR, ItemNames.PREDATOR, ItemNames.WIDOW_MINE,
ItemNames.CYCLONE, ItemNames.WARHOUND,
}
STARPORT_UNITS = {
ItemNames.MEDIVAC, ItemNames.WRAITH, ItemNames.VIKING, ItemNames.BANSHEE,
ItemNames.BATTLECRUISER, ItemNames.HERCULES, ItemNames.SCIENCE_VESSEL, ItemNames.RAVEN,
ItemNames.LIBERATOR, ItemNames.VALKYRIE,
}
def filter_missions(world: World) -> Dict[MissionPools, List[SC2Mission]]:
"""
Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets
"""
world: World = world
mission_order_type = get_option_value(world, "mission_order")
shuffle_no_build = get_option_value(world, "shuffle_no_build")
enabled_campaigns = get_enabled_campaigns(world)
grant_story_tech = get_option_value(world, "grant_story_tech") == GrantStoryTech.option_true
grant_story_levels = get_option_value(world, "grant_story_levels") != GrantStoryLevels.option_disabled
extra_locations = get_option_value(world, "extra_locations")
excluded_missions: Set[SC2Mission] = get_excluded_missions(world)
mission_pools: Dict[MissionPools, List[SC2Mission]] = {}
for mission in SC2Mission:
if not mission_pools.get(mission.pool):
mission_pools[mission.pool] = list()
mission_pools[mission.pool].append(mission)
# A bit of safeguard:
for mission_pool in MissionPools:
if not mission_pools.get(mission_pool):
mission_pools[mission_pool] = []
if mission_order_type == MissionOrder.option_vanilla:
# Vanilla uses the entire mission pool
goal_priorities: Dict[SC2Campaign, SC2CampaignGoalPriority] = {campaign: get_campaign_goal_priority(campaign) for campaign in enabled_campaigns}
goal_level = max(goal_priorities.values())
candidate_campaigns = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
goal_campaign = world.random.choice(candidate_campaigns)
if campaign_final_mission_locations[goal_campaign] is not None:
mission_pools[MissionPools.FINAL] = [campaign_final_mission_locations[goal_campaign].mission]
else:
mission_pools[MissionPools.FINAL] = [list(campaign_alt_final_mission_locations[goal_campaign].keys())[0]]
remove_final_mission_from_other_pools(mission_pools)
return mission_pools
# Finding the goal map
goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns}
goal_level = max(goal_priorities.values())
candidate_campaigns = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
goal_campaign = world.random.choice(candidate_campaigns)
primary_goal = campaign_final_mission_locations[goal_campaign]
if primary_goal is None or primary_goal.mission in excluded_missions:
# No primary goal or its mission is excluded
candidate_missions = list(campaign_alt_final_mission_locations[goal_campaign].keys())
candidate_missions = [mission for mission in candidate_missions if mission not in excluded_missions]
if len(candidate_missions) == 0:
raise Exception("There are no valid goal missions. Please exclude fewer missions.")
goal_mission = world.random.choice(candidate_missions)
else:
goal_mission = primary_goal.mission
# Excluding missions
for difficulty, mission_pool in mission_pools.items():
mission_pools[difficulty] = [mission for mission in mission_pool if mission not in excluded_missions]
mission_pools[MissionPools.FINAL] = [goal_mission]
# Mission pool changes
adv_tactics = get_option_value(world, "required_tactics") != RequiredTactics.option_standard
def move_mission(mission: SC2Mission, current_pool, new_pool):
if mission in mission_pools[current_pool]:
mission_pools[current_pool].remove(mission)
mission_pools[new_pool].append(mission)
# WoL
if shuffle_no_build == ShuffleNoBuild.option_false or adv_tactics:
# Replacing No Build missions with Easy missions
# WoL
move_mission(SC2Mission.ZERO_HOUR, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.EVACUATION, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.DEVILS_PLAYGROUND, MissionPools.EASY, MissionPools.STARTER)
# LotV
move_mission(SC2Mission.THE_GROWING_SHADOW, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.THE_SPEAR_OF_ADUN, MissionPools.EASY, MissionPools.STARTER)
if extra_locations == ExtraLocations.option_enabled:
move_mission(SC2Mission.SKY_SHIELD, MissionPools.EASY, MissionPools.STARTER)
# Pushing this to Easy
move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY, MissionPools.MEDIUM, MissionPools.EASY)
if shuffle_no_build == ShuffleNoBuild.option_false:
# Pushing Outbreak to Normal, as it cannot be placed as the second mission on Build-Only
move_mission(SC2Mission.OUTBREAK, MissionPools.EASY, MissionPools.MEDIUM)
# Pushing extra Normal missions to Easy
move_mission(SC2Mission.ECHOES_OF_THE_FUTURE, MissionPools.MEDIUM, MissionPools.EASY)
move_mission(SC2Mission.CUTTHROAT, MissionPools.MEDIUM, MissionPools.EASY)
# Additional changes on Advanced Tactics
if adv_tactics:
# WoL
move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.SMASH_AND_GRAB, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.THE_MOEBIUS_FACTOR, MissionPools.MEDIUM, MissionPools.EASY)
move_mission(SC2Mission.WELCOME_TO_THE_JUNGLE, MissionPools.MEDIUM, MissionPools.EASY)
move_mission(SC2Mission.ENGINE_OF_DESTRUCTION, MissionPools.HARD, MissionPools.MEDIUM)
# LotV
move_mission(SC2Mission.AMON_S_REACH, MissionPools.EASY, MissionPools.STARTER)
# Prophecy needs to be adjusted on tiny grid
if enabled_campaigns == {SC2Campaign.PROPHECY} and mission_order_type == MissionOrder.option_tiny_grid:
move_mission(SC2Mission.A_SINISTER_TURN, MissionPools.MEDIUM, MissionPools.EASY)
# Prologue's only valid starter is the goal mission
if enabled_campaigns == {SC2Campaign.PROLOGUE} \
or mission_order_type in campaign_depending_orders \
and get_option_value(world, "shuffle_campaigns") == ShuffleCampaigns.option_false:
move_mission(SC2Mission.DARK_WHISPERS, MissionPools.EASY, MissionPools.STARTER)
# HotS
kerriganless = get_option_value(world, "kerrigan_presence") not in kerrigan_unit_available \
or SC2Campaign.HOTS not in enabled_campaigns
if adv_tactics:
# Medium -> Easy
for mission in (SC2Mission.FIRE_IN_THE_SKY, SC2Mission.WAKING_THE_ANCIENT, SC2Mission.CONVICTION):
move_mission(mission, MissionPools.MEDIUM, MissionPools.EASY)
# Hard -> Medium
move_mission(SC2Mission.PHANTOMS_OF_THE_VOID, MissionPools.HARD, MissionPools.MEDIUM)
if not kerriganless:
# Additional starter mission assuming player starts with minimal anti-air
move_mission(SC2Mission.WAKING_THE_ANCIENT, MissionPools.EASY, MissionPools.STARTER)
if grant_story_tech:
# Additional starter mission if player is granted story tech
move_mission(SC2Mission.ENEMY_WITHIN, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.TEMPLAR_S_RETURN, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.THE_ESCAPE, MissionPools.MEDIUM, MissionPools.STARTER)
move_mission(SC2Mission.IN_THE_ENEMY_S_SHADOW, MissionPools.MEDIUM, MissionPools.STARTER)
if (grant_story_tech and grant_story_levels) or kerriganless:
# The player has, all the stuff he needs, provided under these settings
move_mission(SC2Mission.SUPREME, MissionPools.MEDIUM, MissionPools.STARTER)
move_mission(SC2Mission.THE_INFINITE_CYCLE, MissionPools.HARD, MissionPools.STARTER)
if get_option_value(world, "take_over_ai_allies") == TakeOverAIAllies.option_true:
move_mission(SC2Mission.HARBINGER_OF_OBLIVION, MissionPools.MEDIUM, MissionPools.STARTER)
if len(mission_pools[MissionPools.STARTER]) < 2 and not kerriganless or adv_tactics:
# Conditionally moving Easy missions to Starter
move_mission(SC2Mission.HARVEST_OF_SCREAMS, MissionPools.EASY, MissionPools.STARTER)
move_mission(SC2Mission.DOMINATION, MissionPools.EASY, MissionPools.STARTER)
if len(mission_pools[MissionPools.STARTER]) < 2:
move_mission(SC2Mission.TEMPLAR_S_RETURN, MissionPools.EASY, MissionPools.STARTER)
if len(mission_pools[MissionPools.STARTER]) + len(mission_pools[MissionPools.EASY]) < 2:
# Flashpoint needs just a few items at start but competent comp at the end
move_mission(SC2Mission.FLASHPOINT, MissionPools.HARD, MissionPools.EASY)
remove_final_mission_from_other_pools(mission_pools)
return mission_pools
def remove_final_mission_from_other_pools(mission_pools: Dict[MissionPools, List[SC2Mission]]):
final_missions = mission_pools[MissionPools.FINAL]
for pool, missions in mission_pools.items():
if pool == MissionPools.FINAL:
continue
for final_mission in final_missions:
while final_mission in missions:
missions.remove(final_mission)
def get_item_upgrades(inventory: List[Item], parent_item: Union[Item, str]) -> List[Item]:
item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
return [
inv_item for inv_item in inventory
if get_full_item_list()[inv_item.name].parent_item == item_name
]
def get_item_quantity(item: Item, world: World):
if (not get_option_value(world, "nco_items")) \
and SC2Campaign.NCO in get_disabled_campaigns(world) \
and item.name in progressive_if_nco:
return 1
if (not get_option_value(world, "ext_items")) \
and item.name in progressive_if_ext:
return 1
return get_full_item_list()[item.name].quantity
def copy_item(item: Item):
return Item(item.name, item.classification, item.code, item.player)
def num_missions(world: World) -> int:
mission_order_type = get_option_value(world, "mission_order")
if mission_order_type != MissionOrder.option_grid:
mission_order = mission_orders[mission_order_type]()
misssions = [mission for campaign in mission_order for mission in mission_order[campaign]]
return len(misssions) - 1 # Menu
else:
mission_pools = filter_missions(world)
return sum(len(pool) for _, pool in mission_pools.items())
class ValidInventory:
def has(self, item: str, player: int):
return item in self.logical_inventory
def has_any(self, items: Set[str], player: int):
return any(item in self.logical_inventory for item in items)
def has_all(self, items: Set[str], player: int):
return all(item in self.logical_inventory for item in items)
def has_group(self, item_group: str, player: int, count: int = 1):
return False # Deliberately fails here, as item pooling is not aware about mission layout
def count_group(self, item_name_group: str, player: int) -> int:
return 0 # For item filtering assume no missions are beaten
def count(self, item: str, player: int) -> int:
return len([inventory_item for inventory_item in self.logical_inventory if inventory_item == item])
def has_units_per_structure(self) -> bool:
return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure
def generate_reduced_inventory(self, inventory_size: int, mission_requirements: List[Tuple[str, Callable]]) -> List[Item]:
"""Attempts to generate a reduced inventory that can fulfill the mission requirements."""
inventory = list(self.item_pool)
locked_items = list(self.locked_items)
item_list = get_full_item_list()
self.logical_inventory = [
item.name for item in inventory + locked_items + self.existing_items
if item_list[item.name].is_important_for_filtering() # Track all Progression items and those with complex rules for filtering
]
requirements = mission_requirements
parent_items = self.item_children.keys()
parent_lookup = {child: parent for parent, children in self.item_children.items() for child in children}
minimum_upgrades = get_option_value(self.world, "min_number_of_upgrades")
def attempt_removal(item: Item) -> bool:
inventory.remove(item)
# Only run logic checks when removing logic items
if item.name in self.logical_inventory:
self.logical_inventory.remove(item.name)
if not all(requirement(self) for (_, requirement) in mission_requirements):
# If item cannot be removed, lock or revert
self.logical_inventory.append(item.name)
for _ in range(get_item_quantity(item, self.world)):
locked_items.append(copy_item(item))
return False
return True
# Limit the maximum number of upgrades
maxNbUpgrade = get_option_value(self.world, "max_number_of_upgrades")
if maxNbUpgrade != -1:
unit_avail_upgrades = {}
# Needed to take into account locked/existing items
unit_nb_upgrades = {}
for item in inventory:
cItem = item_list[item.name]
if item.name in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
unit_avail_upgrades[item.name] = []
unit_nb_upgrades[item.name] = 0
elif cItem.parent_item is not None:
if cItem.parent_item not in unit_avail_upgrades:
unit_avail_upgrades[cItem.parent_item] = [item]
unit_nb_upgrades[cItem.parent_item] = 1
else:
unit_avail_upgrades[cItem.parent_item].append(item)
unit_nb_upgrades[cItem.parent_item] += 1
# For those two categories, we count them but dont include them in removal
for item in locked_items + self.existing_items:
cItem = item_list[item.name]
if item.name in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
unit_avail_upgrades[item.name] = []
unit_nb_upgrades[item.name] = 0
elif cItem.parent_item is not None:
if cItem.parent_item not in unit_avail_upgrades:
unit_nb_upgrades[cItem.parent_item] = 1
else:
unit_nb_upgrades[cItem.parent_item] += 1
# Making sure that the upgrades being removed is random
shuffled_unit_upgrade_list = list(unit_avail_upgrades.keys())
self.world.random.shuffle(shuffled_unit_upgrade_list)
for unit in shuffled_unit_upgrade_list:
while (unit_nb_upgrades[unit] > maxNbUpgrade) \
and (len(unit_avail_upgrades[unit]) > 0):
itemCandidate = self.world.random.choice(unit_avail_upgrades[unit])
success = attempt_removal(itemCandidate)
# Whatever it succeed to remove the iventory or it fails and thus
# lock it, the upgrade is no longer available for removal
unit_avail_upgrades[unit].remove(itemCandidate)
if success:
unit_nb_upgrades[unit] -= 1
# Locking minimum upgrades for items that have already been locked/placed when minimum required
if minimum_upgrades > 0:
known_items = self.existing_items + locked_items
known_parents = [item for item in known_items if item in parent_items]
for parent in known_parents:
child_items = self.item_children[parent]
removable_upgrades = [item for item in inventory if item in child_items]
locked_upgrade_count = sum(1 if item in child_items else 0 for item in known_items)
self.world.random.shuffle(removable_upgrades)
while len(removable_upgrades) > 0 and locked_upgrade_count < minimum_upgrades:
item_to_lock = removable_upgrades.pop()
inventory.remove(item_to_lock)
locked_items.append(copy_item(item_to_lock))
locked_upgrade_count += 1
if self.min_units_per_structure > 0 and self.has_units_per_structure():
requirements.append(("Minimum units per structure", lambda state: state.has_units_per_structure()))
# Determining if the full-size inventory can complete campaign
failed_locations: List[str] = [location for (location, requirement) in requirements if not requirement(self)]
if len(failed_locations) > 0:
raise Exception(f"Too many items excluded - couldn't satisfy access rules for the following locations:\n{failed_locations}")
# Optionally locking generic items
generic_items = [item for item in inventory if item.name in second_pass_placeable_items]
reserved_generic_percent = get_option_value(self.world, "ensure_generic_items") / 100
reserved_generic_amount = int(len(generic_items) * reserved_generic_percent)
removable_generic_items = []
self.world.random.shuffle(generic_items)
for item in generic_items[:reserved_generic_amount]:
locked_items.append(copy_item(item))
inventory.remove(item)
if item.name not in self.logical_inventory and item.name not in self.locked_items:
removable_generic_items.append(item)
# Main cull process
unused_items = [] # Reusable items for the second pass
while len(inventory) + len(locked_items) > inventory_size:
if len(inventory) == 0:
# There are more items than locations and all of them are already locked due to YAML or logic.
# First, drop non-logic generic items to free up space
while len(removable_generic_items) > 0 and len(locked_items) > inventory_size:
removed_item = removable_generic_items.pop()
locked_items.remove(removed_item)
# If there still isn't enough space, push locked items into start inventory
self.world.random.shuffle(locked_items)
while len(locked_items) > inventory_size:
item: Item = locked_items.pop()
self.multiworld.push_precollected(item)
break
# Select random item from removable items
item = self.world.random.choice(inventory)
# Do not remove item if it would drop upgrades below minimum
if minimum_upgrades > 0:
parent_item = parent_lookup.get(item, None)
if parent_item:
count = sum(1 if item in self.item_children[parent_item] else 0 for item in inventory + locked_items)
if count <= minimum_upgrades:
if parent_item in inventory:
# Attempt to remove parent instead, if possible
item = parent_item
else:
# Lock remaining upgrades
for item in self.item_children[parent_item]:
if item in inventory:
inventory.remove(item)
locked_items.append(copy_item(item))
continue
# Drop child items when removing a parent
if item in parent_items:
items_to_remove = [item for item in self.item_children[item] if item in inventory]
success = attempt_removal(item)
if success:
while len(items_to_remove) > 0:
item_to_remove = items_to_remove.pop()
if item_to_remove not in inventory:
continue
attempt_removal(item_to_remove)
else:
# Unimportant upgrades may be added again in the second pass
if attempt_removal(item):
unused_items.append(item.name)
# Removing extra dependencies
# WoL
logical_inventory_set = set(self.logical_inventory)
if not spider_mine_sources & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Spider Mine)")]
if not BARRACKS_UNITS & logical_inventory_set:
inventory = [item for item in inventory if
not (item.name.startswith(ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX) or item.name == ItemNames.ORBITAL_STRIKE)]
if not FACTORY_UNITS & logical_inventory_set:
inventory = [item for item in inventory if not item.name.startswith(ItemNames.TERRAN_VEHICLE_UPGRADE_PREFIX)]
if not STARPORT_UNITS & logical_inventory_set:
inventory = [item for item in inventory if not item.name.startswith(ItemNames.TERRAN_SHIP_UPGRADE_PREFIX)]
# HotS
# Baneling without sources => remove Baneling and upgrades
if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory
and ItemNames.ZERGLING not in self.logical_inventory
and ItemNames.KERRIGAN_SPAWN_BANELINGS not in self.logical_inventory
):
inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT]
# Spawn Banelings without Zergling => remove Baneling unit, keep upgrades except macro ones
if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory
and ItemNames.ZERGLING not in self.logical_inventory
and ItemNames.KERRIGAN_SPAWN_BANELINGS in self.logical_inventory
):
inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT]
inventory = [item for item in inventory if item.name != ItemNames.BANELING_RAPID_METAMORPH]
if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR, ItemNames.SCOURGE} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
locked_items = [item for item in locked_items if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
# T3 items removal rules - remove morph and its upgrades if the basic unit isn't in
if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Mutalisk/Corruptor)")]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT]
if ItemNames.ROACH not in logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.ROACH_RAVAGER_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT]
if ItemNames.HYDRALISK not in logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Hydralisk)")]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_IMPALER_ASPECT]
# LotV
# Shared unit upgrades between several units
if not {ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Stalker/Instigator/Slayer)")]
if not {ItemNames.PHOENIX, ItemNames.MIRAGE} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Phoenix/Mirage)")]
if not {ItemNames.VOID_RAY, ItemNames.DESTROYER} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Void Ray/Destroyer)")]
if not {ItemNames.IMMORTAL, ItemNames.ANNIHILATOR} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Immortal/Annihilator)")]
if not {ItemNames.DARK_TEMPLAR, ItemNames.AVENGER, ItemNames.BLOOD_HUNTER} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Dark Templar/Avenger/Blood Hunter)")]
if not {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ASCENDANT, ItemNames.DARK_TEMPLAR} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Archon)")]
logical_inventory_set.difference_update([item_name for item_name in logical_inventory_set if item_name.endswith("(Archon)")])
if not {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ARCHON_HIGH_ARCHON} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(High Templar/Signifier)")]
if ItemNames.SUPPLICANT not in logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.ASCENDANT_POWER_OVERWHELMING]
if not {ItemNames.DARK_ARCHON, ItemNames.DARK_TEMPLAR_DARK_ARCHON_MELD} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Dark Archon)")]
if not {ItemNames.SENTRY, ItemNames.ENERGIZER, ItemNames.HAVOC} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Sentry/Energizer/Havoc)")]
if not {ItemNames.SENTRY, ItemNames.ENERGIZER, ItemNames.HAVOC, ItemNames.SHIELD_BATTERY} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Sentry/Energizer/Havoc/Shield Battery)")]
if not {ItemNames.ZEALOT, ItemNames.CENTURION, ItemNames.SENTINEL} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Zealot/Sentinel/Centurion)")]
# Static defense upgrades only if static defense present
if not {ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH, ItemNames.NEXUS_OVERCHARGE, ItemNames.SHIELD_BATTERY} & logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.ENHANCED_TARGETING]
if not {ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH, ItemNames.NEXUS_OVERCHARGE} & logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.OPTIMIZED_ORDNANCE]
# Cull finished, adding locked items back into inventory
inventory += locked_items
# Replacing empty space with generically useful items
replacement_items = [item for item in self.item_pool
if (item not in inventory
and item not in self.locked_items
and (
item.name in second_pass_placeable_items
or item.name in unused_items))]
self.world.random.shuffle(replacement_items)
while len(inventory) < inventory_size and len(replacement_items) > 0:
item = replacement_items.pop()
inventory.append(item)
return inventory
def __init__(self, world: World ,
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item],
used_races: Set[SC2Race], nova_equipment_used: bool):
self.multiworld = world.multiworld
self.player = world.player
self.world: World = world
self.logical_inventory = list()
self.locked_items = locked_items[:]
self.existing_items = existing_items
soa_presence = get_option_value(world, "spear_of_adun_presence")
soa_autocast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence")
# Initial filter of item pool
self.item_pool = []
item_quantities: dict[str, int] = dict()
# Inventory restrictiveness based on number of missions with checks
mission_count = num_missions(world)
self.min_units_per_structure = int(mission_count / 7)
min_upgrades = 1 if mission_count < 10 else 2
for item in item_pool:
item_info = get_full_item_list()[item.name]
if item_info.race != SC2Race.ANY and item_info.race not in used_races:
if soa_presence == SpearOfAdunPresence.option_everywhere \
and item.name in spear_of_adun_calldowns:
# Add SoA powers regardless of used races as it's present everywhere
self.item_pool.append(item)
if soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_everywhere \
and item.name in spear_of_adun_castable_passives:
self.item_pool.append(item)
# Drop any item belonging to a race not used in the campaign
continue
if item.name in nova_equipment and not nova_equipment_used:
# Drop Nova equipment if there's no NCO mission generated
continue
if item_info.type == "Upgrade":
# Locking upgrades based on mission duration
if item.name not in item_quantities:
item_quantities[item.name] = 0
item_quantities[item.name] += 1
if item_quantities[item.name] <= min_upgrades:
self.locked_items.append(item)
else:
self.item_pool.append(item)
elif item_info.type == "Goal":
self.locked_items.append(item)
else:
self.item_pool.append(item)
self.item_children: Dict[Item, List[Item]] = dict()
for item in self.item_pool + locked_items + existing_items:
if item.name in UPGRADABLE_ITEMS:
self.item_children[item] = get_item_upgrades(self.item_pool, item)
def filter_items(world: World, mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]], location_cache: List[Location],
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item]) -> List[Item]:
"""
Returns a semi-randomly pruned set of items based on number of available locations.
The returned inventory must be capable of logically accessing every location in the world.
"""
open_locations = [location for location in location_cache if location.item is None]
inventory_size = len(open_locations)
used_races = get_used_races(mission_req_table, world)
nova_equipment_used = is_nova_equipment_used(mission_req_table)
mission_requirements = [(location.name, location.access_rule) for location in location_cache]
valid_inventory = ValidInventory(world, item_pool, existing_items, locked_items, used_races, nova_equipment_used)
valid_items = valid_inventory.generate_reduced_inventory(inventory_size, mission_requirements)
return valid_items
def get_used_races(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]], world: World) -> Set[SC2Race]:
grant_story_tech = get_option_value(world, "grant_story_tech")
take_over_ai_allies = get_option_value(world, "take_over_ai_allies")
kerrigan_presence = get_option_value(world, "kerrigan_presence") \
and SC2Campaign.HOTS in get_enabled_campaigns(world)
missions = missions_in_mission_table(mission_req_table)
# By missions
races = set([mission.race for mission in missions])
# Conditionally logic-less no-builds (They're set to SC2Race.ANY):
if grant_story_tech == GrantStoryTech.option_false:
if SC2Mission.ENEMY_WITHIN in missions:
# Zerg units need to be unlocked
races.add(SC2Race.ZERG)
if kerrigan_presence in kerrigan_unit_available \
and not missions.isdisjoint({SC2Mission.BACK_IN_THE_SADDLE, SC2Mission.SUPREME, SC2Mission.CONVICTION, SC2Mission.THE_INFINITE_CYCLE}):
# You need some Kerrigan abilities (they're granted if Kerriganless or story tech granted)
races.add(SC2Race.ZERG)
# If you take over the AI Ally, you need to have its race stuff
if take_over_ai_allies == TakeOverAIAllies.option_true \
and not missions.isdisjoint({SC2Mission.THE_RECKONING}):
# Jimmy in The Reckoning
races.add(SC2Race.TERRAN)
return races
def is_nova_equipment_used(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]]) -> bool:
missions = missions_in_mission_table(mission_req_table)
return any([mission.campaign == SC2Campaign.NCO for mission in missions])
def missions_in_mission_table(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]]) -> Set[SC2Mission]:
return set([mission.mission for campaign_missions in mission_req_table.values() for mission in
campaign_missions.values()])

691
worlds/sc2/Regions.py Normal file
View File

@ -0,0 +1,691 @@
from typing import List, Dict, Tuple, Optional, Callable, NamedTuple, Union
import math
from BaseClasses import MultiWorld, Region, Entrance, Location, CollectionState
from .Locations import LocationData
from .Options import get_option_value, MissionOrder, get_enabled_campaigns, campaign_depending_orders, \
GridTwoStartPositions
from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, \
MissionPools, SC2Campaign, get_goal_location, SC2Mission, MissionConnection
from .PoolFilter import filter_missions
from worlds.AutoWorld import World
class SC2MissionSlot(NamedTuple):
campaign: SC2Campaign
slot: Union[MissionPools, SC2Mission, None]
def create_regions(
world: World, locations: Tuple[LocationData, ...], location_cache: List[Location]
) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
"""
Creates region connections by calling the multiworld's `connect()` methods
Returns a 3-tuple containing:
* dict[SC2Campaign, Dict[str, MissionInfo]] mapping a campaign and mission name to its data
* int The number of missions in the world
* str The name of the goal location
"""
mission_order_type: int = get_option_value(world, "mission_order")
if mission_order_type == MissionOrder.option_vanilla:
return create_vanilla_regions(world, locations, location_cache)
elif mission_order_type == MissionOrder.option_grid:
return create_grid_regions(world, locations, location_cache)
else:
return create_structured_regions(world, locations, location_cache, mission_order_type)
def create_vanilla_regions(
world: World,
locations: Tuple[LocationData, ...],
location_cache: List[Location],
) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
locations_per_region = get_locations_per_region(locations)
regions = [create_region(world, locations_per_region, location_cache, "Menu")]
mission_pools: Dict[MissionPools, List[SC2Mission]] = filter_missions(world)
final_mission = mission_pools[MissionPools.FINAL][0]
enabled_campaigns = get_enabled_campaigns(world)
names: Dict[str, int] = {}
# Generating all regions and locations for each enabled campaign
for campaign in enabled_campaigns:
for region_name in vanilla_mission_req_table[campaign].keys():
regions.append(create_region(world, locations_per_region, location_cache, region_name))
world.multiworld.regions += regions
vanilla_mission_reqs = {campaign: missions for campaign, missions in vanilla_mission_req_table.items() if campaign in enabled_campaigns}
def wol_cleared_missions(state: CollectionState, mission_count: int) -> bool:
return state.has_group("WoL Missions", world.player, mission_count)
player: int = world.player
if SC2Campaign.WOL in enabled_campaigns:
connect(world, names, 'Menu', 'Liberation Day')
connect(world, names, 'Liberation Day', 'The Outlaws',
lambda state: state.has("Beat Liberation Day", player))
connect(world, names, 'The Outlaws', 'Zero Hour',
lambda state: state.has("Beat The Outlaws", player))
connect(world, names, 'Zero Hour', 'Evacuation',
lambda state: state.has("Beat Zero Hour", player))
connect(world, names, 'Evacuation', 'Outbreak',
lambda state: state.has("Beat Evacuation", player))
connect(world, names, "Outbreak", "Safe Haven",
lambda state: wol_cleared_missions(state, 7) and state.has("Beat Outbreak", player))
connect(world, names, "Outbreak", "Haven's Fall",
lambda state: wol_cleared_missions(state, 7) and state.has("Beat Outbreak", player))
connect(world, names, 'Zero Hour', 'Smash and Grab',
lambda state: state.has("Beat Zero Hour", player))
connect(world, names, 'Smash and Grab', 'The Dig',
lambda state: wol_cleared_missions(state, 8) and state.has("Beat Smash and Grab", player))
connect(world, names, 'The Dig', 'The Moebius Factor',
lambda state: wol_cleared_missions(state, 11) and state.has("Beat The Dig", player))
connect(world, names, 'The Moebius Factor', 'Supernova',
lambda state: wol_cleared_missions(state, 14) and state.has("Beat The Moebius Factor", player))
connect(world, names, 'Supernova', 'Maw of the Void',
lambda state: state.has("Beat Supernova", player))
connect(world, names, 'Zero Hour', "Devil's Playground",
lambda state: wol_cleared_missions(state, 4) and state.has("Beat Zero Hour", player))
connect(world, names, "Devil's Playground", 'Welcome to the Jungle',
lambda state: state.has("Beat Devil's Playground", player))
connect(world, names, "Welcome to the Jungle", 'Breakout',
lambda state: wol_cleared_missions(state, 8) and state.has("Beat Welcome to the Jungle", player))
connect(world, names, "Welcome to the Jungle", 'Ghost of a Chance',
lambda state: wol_cleared_missions(state, 8) and state.has("Beat Welcome to the Jungle", player))
connect(world, names, "Zero Hour", 'The Great Train Robbery',
lambda state: wol_cleared_missions(state, 6) and state.has("Beat Zero Hour", player))
connect(world, names, 'The Great Train Robbery', 'Cutthroat',
lambda state: state.has("Beat The Great Train Robbery", player))
connect(world, names, 'Cutthroat', 'Engine of Destruction',
lambda state: state.has("Beat Cutthroat", player))
connect(world, names, 'Engine of Destruction', 'Media Blitz',
lambda state: state.has("Beat Engine of Destruction", player))
connect(world, names, 'Media Blitz', 'Piercing the Shroud',
lambda state: state.has("Beat Media Blitz", player))
connect(world, names, 'Maw of the Void', 'Gates of Hell',
lambda state: state.has("Beat Maw of the Void", player))
connect(world, names, 'Gates of Hell', 'Belly of the Beast',
lambda state: state.has("Beat Gates of Hell", player))
connect(world, names, 'Gates of Hell', 'Shatter the Sky',
lambda state: state.has("Beat Gates of Hell", player))
connect(world, names, 'Gates of Hell', 'All-In',
lambda state: state.has('Beat Gates of Hell', player) and (
state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player)))
if SC2Campaign.PROPHECY in enabled_campaigns:
if SC2Campaign.WOL in enabled_campaigns:
connect(world, names, 'The Dig', 'Whispers of Doom',
lambda state: state.has("Beat The Dig", player)),
else:
vanilla_mission_reqs[SC2Campaign.PROPHECY] = vanilla_mission_reqs[SC2Campaign.PROPHECY].copy()
vanilla_mission_reqs[SC2Campaign.PROPHECY][SC2Mission.WHISPERS_OF_DOOM.mission_name] = MissionInfo(
SC2Mission.WHISPERS_OF_DOOM, [], SC2Mission.WHISPERS_OF_DOOM.area)
connect(world, names, 'Menu', 'Whispers of Doom'),
connect(world, names, 'Whispers of Doom', 'A Sinister Turn',
lambda state: state.has("Beat Whispers of Doom", player))
connect(world, names, 'A Sinister Turn', 'Echoes of the Future',
lambda state: state.has("Beat A Sinister Turn", player))
connect(world, names, 'Echoes of the Future', 'In Utter Darkness',
lambda state: state.has("Beat Echoes of the Future", player))
if SC2Campaign.HOTS in enabled_campaigns:
connect(world, names, 'Menu', 'Lab Rat'),
connect(world, names, 'Lab Rat', 'Back in the Saddle',
lambda state: state.has("Beat Lab Rat", player)),
connect(world, names, 'Back in the Saddle', 'Rendezvous',
lambda state: state.has("Beat Back in the Saddle", player)),
connect(world, names, 'Rendezvous', 'Harvest of Screams',
lambda state: state.has("Beat Rendezvous", player)),
connect(world, names, 'Harvest of Screams', 'Shoot the Messenger',
lambda state: state.has("Beat Harvest of Screams", player)),
connect(world, names, 'Shoot the Messenger', 'Enemy Within',
lambda state: state.has("Beat Shoot the Messenger", player)),
connect(world, names, 'Rendezvous', 'Domination',
lambda state: state.has("Beat Rendezvous", player)),
connect(world, names, 'Domination', 'Fire in the Sky',
lambda state: state.has("Beat Domination", player)),
connect(world, names, 'Fire in the Sky', 'Old Soldiers',
lambda state: state.has("Beat Fire in the Sky", player)),
connect(world, names, 'Old Soldiers', 'Waking the Ancient',
lambda state: state.has("Beat Old Soldiers", player)),
connect(world, names, 'Enemy Within', 'Waking the Ancient',
lambda state: state.has("Beat Enemy Within", player)),
connect(world, names, 'Waking the Ancient', 'The Crucible',
lambda state: state.has("Beat Waking the Ancient", player)),
connect(world, names, 'The Crucible', 'Supreme',
lambda state: state.has("Beat The Crucible", player)),
connect(world, names, 'Supreme', 'Infested',
lambda state: state.has("Beat Supreme", player) and
state.has("Beat Old Soldiers", player) and
state.has("Beat Enemy Within", player)),
connect(world, names, 'Infested', 'Hand of Darkness',
lambda state: state.has("Beat Infested", player)),
connect(world, names, 'Hand of Darkness', 'Phantoms of the Void',
lambda state: state.has("Beat Hand of Darkness", player)),
connect(world, names, 'Supreme', 'With Friends Like These',
lambda state: state.has("Beat Supreme", player) and
state.has("Beat Old Soldiers", player) and
state.has("Beat Enemy Within", player)),
connect(world, names, 'With Friends Like These', 'Conviction',
lambda state: state.has("Beat With Friends Like These", player)),
connect(world, names, 'Conviction', 'Planetfall',
lambda state: state.has("Beat Conviction", player) and
state.has("Beat Phantoms of the Void", player)),
connect(world, names, 'Planetfall', 'Death From Above',
lambda state: state.has("Beat Planetfall", player)),
connect(world, names, 'Death From Above', 'The Reckoning',
lambda state: state.has("Beat Death From Above", player)),
if SC2Campaign.PROLOGUE in enabled_campaigns:
connect(world, names, "Menu", "Dark Whispers")
connect(world, names, "Dark Whispers", "Ghosts in the Fog",
lambda state: state.has("Beat Dark Whispers", player))
connect(world, names, "Dark Whispers", "Evil Awoken",
lambda state: state.has("Beat Ghosts in the Fog", player))
if SC2Campaign.LOTV in enabled_campaigns:
connect(world, names, "Menu", "For Aiur!")
connect(world, names, "For Aiur!", "The Growing Shadow",
lambda state: state.has("Beat For Aiur!", player)),
connect(world, names, "The Growing Shadow", "The Spear of Adun",
lambda state: state.has("Beat The Growing Shadow", player)),
connect(world, names, "The Spear of Adun", "Sky Shield",
lambda state: state.has("Beat The Spear of Adun", player)),
connect(world, names, "Sky Shield", "Brothers in Arms",
lambda state: state.has("Beat Sky Shield", player)),
connect(world, names, "Brothers in Arms", "Forbidden Weapon",
lambda state: state.has("Beat Brothers in Arms", player)),
connect(world, names, "The Spear of Adun", "Amon's Reach",
lambda state: state.has("Beat The Spear of Adun", player)),
connect(world, names, "Amon's Reach", "Last Stand",
lambda state: state.has("Beat Amon's Reach", player)),
connect(world, names, "Last Stand", "Forbidden Weapon",
lambda state: state.has("Beat Last Stand", player)),
connect(world, names, "Forbidden Weapon", "Temple of Unification",
lambda state: state.has("Beat Brothers in Arms", player)
and state.has("Beat Last Stand", player)
and state.has("Beat Forbidden Weapon", player)),
connect(world, names, "Temple of Unification", "The Infinite Cycle",
lambda state: state.has("Beat Temple of Unification", player)),
connect(world, names, "The Infinite Cycle", "Harbinger of Oblivion",
lambda state: state.has("Beat The Infinite Cycle", player)),
connect(world, names, "Harbinger of Oblivion", "Unsealing the Past",
lambda state: state.has("Beat Harbinger of Oblivion", player)),
connect(world, names, "Unsealing the Past", "Purification",
lambda state: state.has("Beat Unsealing the Past", player)),
connect(world, names, "Purification", "Templar's Charge",
lambda state: state.has("Beat Purification", player)),
connect(world, names, "Harbinger of Oblivion", "Steps of the Rite",
lambda state: state.has("Beat Harbinger of Oblivion", player)),
connect(world, names, "Steps of the Rite", "Rak'Shir",
lambda state: state.has("Beat Steps of the Rite", player)),
connect(world, names, "Rak'Shir", "Templar's Charge",
lambda state: state.has("Beat Rak'Shir", player)),
connect(world, names, "Templar's Charge", "Templar's Return",
lambda state: state.has("Beat Purification", player)
and state.has("Beat Rak'Shir", player)
and state.has("Beat Templar's Charge", player)),
connect(world, names, "Templar's Return", "The Host",
lambda state: state.has("Beat Templar's Return", player)),
connect(world, names, "The Host", "Salvation",
lambda state: state.has("Beat The Host", player)),
if SC2Campaign.EPILOGUE in enabled_campaigns:
# TODO: Make this aware about excluded campaigns
connect(world, names, "Salvation", "Into the Void",
lambda state: state.has("Beat Salvation", player)
and state.has("Beat The Reckoning", player)
and state.has("Beat All-In", player)),
connect(world, names, "Into the Void", "The Essence of Eternity",
lambda state: state.has("Beat Into the Void", player)),
connect(world, names, "The Essence of Eternity", "Amon's Fall",
lambda state: state.has("Beat The Essence of Eternity", player)),
if SC2Campaign.NCO in enabled_campaigns:
connect(world, names, "Menu", "The Escape")
connect(world, names, "The Escape", "Sudden Strike",
lambda state: state.has("Beat The Escape", player))
connect(world, names, "Sudden Strike", "Enemy Intelligence",
lambda state: state.has("Beat Sudden Strike", player))
connect(world, names, "Enemy Intelligence", "Trouble In Paradise",
lambda state: state.has("Beat Enemy Intelligence", player))
connect(world, names, "Trouble In Paradise", "Night Terrors",
lambda state: state.has("Beat Evacuation", player))
connect(world, names, "Night Terrors", "Flashpoint",
lambda state: state.has("Beat Night Terrors", player))
connect(world, names, "Flashpoint", "In the Enemy's Shadow",
lambda state: state.has("Beat Flashpoint", player))
connect(world, names, "In the Enemy's Shadow", "Dark Skies",
lambda state: state.has("Beat In the Enemy's Shadow", player))
connect(world, names, "Dark Skies", "End Game",
lambda state: state.has("Beat Dark Skies", player))
goal_location = get_goal_location(final_mission)
assert goal_location, f"Unable to find a goal location for mission {final_mission}"
setup_final_location(goal_location, location_cache)
return (vanilla_mission_reqs, final_mission.id, goal_location)
def create_grid_regions(
world: World,
locations: Tuple[LocationData, ...],
location_cache: List[Location],
) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
locations_per_region = get_locations_per_region(locations)
mission_pools = filter_missions(world)
final_mission = mission_pools[MissionPools.FINAL][0]
mission_pool = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
num_missions = min(len(mission_pool), get_option_value(world, "maximum_campaign_size"))
remove_top_left: bool = get_option_value(world, "grid_two_start_positions") == GridTwoStartPositions.option_true
regions = [create_region(world, locations_per_region, location_cache, "Menu")]
names: Dict[str, int] = {}
missions: Dict[Tuple[int, int], SC2Mission] = {}
grid_size_x, grid_size_y, num_corners_to_remove = get_grid_dimensions(num_missions + remove_top_left)
# pick missions in order along concentric diagonals
# each diagonal will have the same difficulty
# this keeps long sides from possibly stealing lower-difficulty missions from future columns
num_diagonals = grid_size_x + grid_size_y - 1
diagonal_difficulty = MissionPools.STARTER
missions_to_add = mission_pools[MissionPools.STARTER]
for diagonal in range(num_diagonals):
if diagonal == num_diagonals - 1:
diagonal_difficulty = MissionPools.FINAL
grid_coords = (grid_size_x-1, grid_size_y-1)
missions[grid_coords] = final_mission
break
if diagonal == 0 and remove_top_left:
continue
diagonal_length = min(diagonal + 1, num_diagonals - diagonal, grid_size_x, grid_size_y)
if len(missions_to_add) < diagonal_length:
raise Exception(f"There are not enough {diagonal_difficulty.name} missions to fill the campaign. Please exclude fewer missions.")
for i in range(diagonal_length):
# (0,0) + (0,1)*diagonal + (1,-1)*i + (1,-1)*max(diagonal - grid_size_y + 1, 0)
grid_coords = (i + max(diagonal - grid_size_y + 1, 0), diagonal - i - max(diagonal - grid_size_y + 1, 0))
if grid_coords == (grid_size_x - 1, 0) and num_corners_to_remove >= 2:
pass
elif grid_coords == (0, grid_size_y - 1) and num_corners_to_remove >= 1:
pass
else:
mission_index = world.random.randint(0, len(missions_to_add) - 1)
missions[grid_coords] = missions_to_add.pop(mission_index)
if diagonal_difficulty < MissionPools.VERY_HARD:
diagonal_difficulty = MissionPools(diagonal_difficulty.value + 1)
missions_to_add.extend(mission_pools[diagonal_difficulty])
# Generating regions and locations from selected missions
for x in range(grid_size_x):
for y in range(grid_size_y):
if missions.get((x, y)):
regions.append(create_region(world, locations_per_region, location_cache, missions[(x, y)].mission_name))
world.multiworld.regions += regions
# This pattern is horrifying, why are we using the dict as an ordered dict???
slot_map: Dict[Tuple[int, int], int] = {}
for index, coords in enumerate(missions):
slot_map[coords] = index + 1
mission_req_table: Dict[str, MissionInfo] = {}
for coords, mission in missions.items():
prepend_vertical = 0
if not mission:
continue
connections: List[MissionConnection] = []
if coords == (0, 0) or (remove_top_left and sum(coords) == 1):
# Connect to the "Menu" starting region
connect(world, names, "Menu", mission.mission_name)
else:
for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
connected_coords = (coords[0] + dx, coords[1] + dy)
if connected_coords in missions:
# connections.append(missions[connected_coords])
connections.append(MissionConnection(slot_map[connected_coords]))
connect(world, names, missions[connected_coords].mission_name, mission.mission_name,
make_grid_connect_rule(missions, connected_coords, world.player),
)
if coords[1] == 1 and not missions.get((coords[0], 0)):
prepend_vertical = 1
mission_req_table[mission.mission_name] = MissionInfo(
mission,
connections,
category=f'_{coords[0] + 1}',
or_requirements=True,
ui_vertical_padding=prepend_vertical,
)
final_mission_id = final_mission.id
# Changing the completion condition for alternate final missions into an event
final_location = get_goal_location(final_mission)
setup_final_location(final_location, location_cache)
return {SC2Campaign.GLOBAL: mission_req_table}, final_mission_id, final_location
def make_grid_connect_rule(
missions: Dict[Tuple[int, int], SC2Mission],
connected_coords: Tuple[int, int],
player: int
) -> Callable[[CollectionState], bool]:
return lambda state: state.has(f"Beat {missions[connected_coords].mission_name}", player)
def create_structured_regions(
world: World,
locations: Tuple[LocationData, ...],
location_cache: List[Location],
mission_order_type: int,
) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
locations_per_region = get_locations_per_region(locations)
mission_order = mission_orders[mission_order_type]()
enabled_campaigns = get_enabled_campaigns(world)
shuffle_campaigns = get_option_value(world, "shuffle_campaigns")
mission_pools: Dict[MissionPools, List[SC2Mission]] = filter_missions(world)
final_mission = mission_pools[MissionPools.FINAL][0]
regions = [create_region(world, locations_per_region, location_cache, "Menu")]
names: Dict[str, int] = {}
mission_slots: List[SC2MissionSlot] = []
mission_pool = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
if mission_order_type in campaign_depending_orders:
# Do slot removal per campaign
for campaign in enabled_campaigns:
campaign_mission_pool = [mission for mission in mission_pool if mission.campaign == campaign]
campaign_mission_pool_size = len(campaign_mission_pool)
removals = len(mission_order[campaign]) - campaign_mission_pool_size
for mission in mission_order[campaign]:
# Removing extra missions if mission pool is too small
if 0 < mission.removal_priority <= removals:
mission_slots.append(SC2MissionSlot(campaign, None))
elif mission.type == MissionPools.FINAL:
if campaign == final_mission.campaign:
# Campaign is elected to be goal
mission_slots.append(SC2MissionSlot(campaign, final_mission))
else:
# Not the goal, find the most difficult mission in the pool and set the difficulty
campaign_difficulty = max(mission.pool for mission in campaign_mission_pool)
mission_slots.append(SC2MissionSlot(campaign, campaign_difficulty))
else:
mission_slots.append(SC2MissionSlot(campaign, mission.type))
else:
order = mission_order[SC2Campaign.GLOBAL]
# Determining if missions must be removed
mission_pool_size = sum(len(mission_pool) for mission_pool in mission_pools.values())
removals = len(order) - mission_pool_size
# Initial fill out of mission list and marking All-In mission
for mission in order:
# Removing extra missions if mission pool is too small
if 0 < mission.removal_priority <= removals:
mission_slots.append(SC2MissionSlot(SC2Campaign.GLOBAL, None))
elif mission.type == MissionPools.FINAL:
mission_slots.append(SC2MissionSlot(SC2Campaign.GLOBAL, final_mission))
else:
mission_slots.append(SC2MissionSlot(SC2Campaign.GLOBAL, mission.type))
no_build_slots = []
easy_slots = []
medium_slots = []
hard_slots = []
very_hard_slots = []
# Search through missions to find slots needed to fill
for i in range(len(mission_slots)):
mission_slot = mission_slots[i]
if mission_slot is None:
continue
if isinstance(mission_slot, SC2MissionSlot):
if mission_slot.slot is None:
continue
if mission_slot.slot == MissionPools.STARTER:
no_build_slots.append(i)
elif mission_slot.slot == MissionPools.EASY:
easy_slots.append(i)
elif mission_slot.slot == MissionPools.MEDIUM:
medium_slots.append(i)
elif mission_slot.slot == MissionPools.HARD:
hard_slots.append(i)
elif mission_slot.slot == MissionPools.VERY_HARD:
very_hard_slots.append(i)
def pick_mission(slot):
if shuffle_campaigns or mission_order_type not in campaign_depending_orders:
# Pick a mission from any campaign
filler = world.random.randint(0, len(missions_to_add) - 1)
mission = missions_to_add.pop(filler)
slot_campaign = mission_slots[slot].campaign
mission_slots[slot] = SC2MissionSlot(slot_campaign, mission)
else:
# Pick a mission from required campaign
slot_campaign = mission_slots[slot].campaign
campaign_mission_candidates = [mission for mission in missions_to_add if mission.campaign == slot_campaign]
mission = world.random.choice(campaign_mission_candidates)
missions_to_add.remove(mission)
mission_slots[slot] = SC2MissionSlot(slot_campaign, mission)
# Add no_build missions to the pool and fill in no_build slots
missions_to_add: List[SC2Mission] = mission_pools[MissionPools.STARTER]
if len(no_build_slots) > len(missions_to_add):
raise Exception("There are no valid No-Build missions. Please exclude fewer missions.")
for slot in no_build_slots:
pick_mission(slot)
# Add easy missions into pool and fill in easy slots
missions_to_add = missions_to_add + mission_pools[MissionPools.EASY]
if len(easy_slots) > len(missions_to_add):
raise Exception("There are not enough Easy missions to fill the campaign. Please exclude fewer missions.")
for slot in easy_slots:
pick_mission(slot)
# Add medium missions into pool and fill in medium slots
missions_to_add = missions_to_add + mission_pools[MissionPools.MEDIUM]
if len(medium_slots) > len(missions_to_add):
raise Exception("There are not enough Easy and Medium missions to fill the campaign. Please exclude fewer missions.")
for slot in medium_slots:
pick_mission(slot)
# Add hard missions into pool and fill in hard slots
missions_to_add = missions_to_add + mission_pools[MissionPools.HARD]
if len(hard_slots) > len(missions_to_add):
raise Exception("There are not enough missions to fill the campaign. Please exclude fewer missions.")
for slot in hard_slots:
pick_mission(slot)
# Add very hard missions into pool and fill in very hard slots
missions_to_add = missions_to_add + mission_pools[MissionPools.VERY_HARD]
if len(very_hard_slots) > len(missions_to_add):
raise Exception("There are not enough missions to fill the campaign. Please exclude fewer missions.")
for slot in very_hard_slots:
pick_mission(slot)
# Generating regions and locations from selected missions
for mission_slot in mission_slots:
if isinstance(mission_slot.slot, SC2Mission):
regions.append(create_region(world, locations_per_region, location_cache, mission_slot.slot.mission_name))
world.multiworld.regions += regions
campaigns: List[SC2Campaign]
if mission_order_type in campaign_depending_orders:
campaigns = list(enabled_campaigns)
else:
campaigns = [SC2Campaign.GLOBAL]
mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = {}
campaign_mission_slots: Dict[SC2Campaign, List[SC2MissionSlot]] = \
{
campaign: [mission_slot for mission_slot in mission_slots if campaign == mission_slot.campaign]
for campaign in campaigns
}
slot_map: Dict[SC2Campaign, List[int]] = dict()
for campaign in campaigns:
mission_req_table.update({campaign: dict()})
# Mapping original mission slots to shifted mission slots when missions are removed
slot_map[campaign] = []
slot_offset = 0
for position, mission in enumerate(campaign_mission_slots[campaign]):
slot_map[campaign].append(position - slot_offset + 1)
if mission is None or mission.slot is None:
slot_offset += 1
def build_connection_rule(mission_names: List[str], missions_req: int) -> Callable:
player = world.player
if len(mission_names) > 1:
return lambda state: state.has_all({f"Beat {name}" for name in mission_names}, player) \
and state.has_group("Missions", player, missions_req)
else:
return lambda state: state.has(f"Beat {mission_names[0]}", player) \
and state.has_group("Missions", player, missions_req)
for campaign in campaigns:
# Loop through missions to create requirements table and connect regions
for i, mission in enumerate(campaign_mission_slots[campaign]):
if mission is None or mission.slot is None:
continue
connections: List[MissionConnection] = []
all_connections: List[SC2MissionSlot] = []
connection: MissionConnection
for connection in mission_order[campaign][i].connect_to:
if connection.connect_to == -1:
continue
# If mission normally connects to an excluded campaign, connect to menu instead
if connection.campaign not in campaign_mission_slots:
connection.connect_to = -1
continue
while campaign_mission_slots[connection.campaign][connection.connect_to].slot is None:
connection.connect_to -= 1
all_connections.append(campaign_mission_slots[connection.campaign][connection.connect_to])
for connection in mission_order[campaign][i].connect_to:
if connection.connect_to == -1:
connect(world, names, "Menu", mission.slot.mission_name)
else:
required_mission = campaign_mission_slots[connection.campaign][connection.connect_to]
if ((required_mission is None or required_mission.slot is None)
and not mission_order[campaign][i].completion_critical): # Drop non-critical null slots
continue
while required_mission is None or required_mission.slot is None: # Substituting null slot with prior slot
connection.connect_to -= 1
required_mission = campaign_mission_slots[connection.campaign][connection.connect_to]
required_missions = [required_mission] if mission_order[campaign][i].or_requirements else all_connections
if isinstance(required_mission.slot, SC2Mission):
required_mission_name = required_mission.slot.mission_name
required_missions_names = [mission.slot.mission_name for mission in required_missions]
connect(world, names, required_mission_name, mission.slot.mission_name,
build_connection_rule(required_missions_names, mission_order[campaign][i].number))
connections.append(MissionConnection(slot_map[connection.campaign][connection.connect_to], connection.campaign))
mission_req_table[campaign].update({mission.slot.mission_name: MissionInfo(
mission.slot, connections, mission_order[campaign][i].category,
number=mission_order[campaign][i].number,
completion_critical=mission_order[campaign][i].completion_critical,
or_requirements=mission_order[campaign][i].or_requirements)})
final_mission_id = final_mission.id
# Changing the completion condition for alternate final missions into an event
final_location = get_goal_location(final_mission)
setup_final_location(final_location, location_cache)
return mission_req_table, final_mission_id, final_location
def setup_final_location(final_location, location_cache):
# Final location should be near the end of the cache
for i in range(len(location_cache) - 1, -1, -1):
if location_cache[i].name == final_location:
location_cache[i].address = None
break
def create_location(player: int, location_data: LocationData, region: Region,
location_cache: List[Location]) -> Location:
location = Location(player, location_data.name, location_data.code, region)
location.access_rule = location_data.rule
location_cache.append(location)
return location
def create_region(world: World, locations_per_region: Dict[str, List[LocationData]],
location_cache: List[Location], name: str) -> Region:
region = Region(name, world.player, world.multiworld)
if name in locations_per_region:
for location_data in locations_per_region[name]:
location = create_location(world.player, location_data, region, location_cache)
region.locations.append(location)
return region
def connect(world: World, used_names: Dict[str, int], source: str, target: str,
rule: Optional[Callable] = None):
source_region = world.get_region(source)
target_region = world.get_region(target)
if target not in used_names:
used_names[target] = 1
name = target
else:
used_names[target] += 1
name = target + (' ' * used_names[target])
connection = Entrance(world.player, name, source_region)
if rule:
connection.access_rule = rule
source_region.exits.append(connection)
connection.connect(target_region)
def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]:
per_region: Dict[str, List[LocationData]] = {}
for location in locations:
per_region.setdefault(location.region, []).append(location)
return per_region
def get_factors(number: int) -> Tuple[int, int]:
"""
Simple factorization into pairs of numbers (x, y) using a sieve method.
Returns the factorization that is most square, i.e. where x + y is minimized.
Factor order is such that x <= y.
"""
assert number > 0
for divisor in range(math.floor(math.sqrt(number)), 1, -1):
quotient = number // divisor
if quotient * divisor == number:
return divisor, quotient
return 1, number
def get_grid_dimensions(size: int) -> Tuple[int, int, int]:
"""
Get the dimensions of a grid mission order from the number of missions, int the format (x, y, error).
* Error will always be 0, 1, or 2, so the missions can be removed from the corners that aren't the start or end.
* Dimensions are chosen such that x <= y, as buttons in the UI are wider than they are tall.
* Dimensions are chosen to be maximally square. That is, x + y + error is minimized.
* If multiple options of the same rating are possible, the one with the larger error is chosen,
as it will appear more square. Compare 3x11 to 5x7-2 for an example of this.
"""
dimension_candidates: List[Tuple[int, int, int]] = [(*get_factors(size + x), x) for x in (2, 1, 0)]
best_dimension = min(dimension_candidates, key=sum)
return best_dimension

952
worlds/sc2/Rules.py Normal file
View File

@ -0,0 +1,952 @@
from typing import Set
from BaseClasses import CollectionState
from .Options import get_option_value, RequiredTactics, kerrigan_unit_available, AllInMap, \
GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence, \
get_enabled_campaigns, MissionOrder
from .Items import get_basic_units, defense_ratings, zerg_defense_ratings, kerrigan_actives, air_defense_ratings, \
kerrigan_levels, get_full_item_list
from .MissionTables import SC2Race, SC2Campaign
from . import ItemNames
from worlds.AutoWorld import World
class SC2Logic:
def lock_any_item(self, state: CollectionState, items: Set[str]) -> bool:
"""
Guarantees that at least one of these items will remain in the world. Doesn't affect placement.
Needed for cases when the dynamic pool filtering could remove all the item prerequisites
:param state:
:param items:
:return:
"""
return self.is_item_placement(state) \
or state.has_any(items, self.player)
def is_item_placement(self, state):
"""
Tells if it's item placement or item pool filter
:param state:
:return: True for item placement, False for pool filter
"""
# has_group with count = 0 is always true for item placement and always false for SC2 item filtering
return state.has_group("Missions", self.player, 0)
# WoL
def terran_common_unit(self, state: CollectionState) -> bool:
return state.has_any(self.basic_terran_units, self.player)
def terran_early_tech(self, state: CollectionState):
"""
Basic combat unit that can be deployed quickly from mission start
:param state
:return:
"""
return (
state.has_any({ItemNames.MARINE, ItemNames.FIREBAT, ItemNames.MARAUDER, ItemNames.REAPER, ItemNames.HELLION}, self.player)
or (self.advanced_tactics and state.has_any({ItemNames.GOLIATH, ItemNames.DIAMONDBACK, ItemNames.VIKING, ItemNames.BANSHEE}, self.player))
)
def terran_air(self, state: CollectionState) -> bool:
"""
Air units or drops on advanced tactics
:param state:
:return:
"""
return (state.has_any({ItemNames.VIKING, ItemNames.WRAITH, ItemNames.BANSHEE, ItemNames.BATTLECRUISER}, self.player) or self.advanced_tactics
and state.has_any({ItemNames.HERCULES, ItemNames.MEDIVAC}, self.player) and self.terran_common_unit(state)
)
def terran_air_anti_air(self, state: CollectionState) -> bool:
"""
Air-to-air
:param state:
:return:
"""
return (
state.has(ItemNames.VIKING, self.player)
or state.has_all({ItemNames.WRAITH, ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player)
or state.has_all({ItemNames.BATTLECRUISER, ItemNames.BATTLECRUISER_ATX_LASER_BATTERY}, self.player)
or self.advanced_tactics and state.has_any({ItemNames.WRAITH, ItemNames.VALKYRIE, ItemNames.BATTLECRUISER}, self.player)
)
def terran_competent_ground_to_air(self, state: CollectionState) -> bool:
"""
Ground-to-air
:param state:
:return:
"""
return (
state.has(ItemNames.GOLIATH, self.player)
or state.has(ItemNames.MARINE, self.player) and self.terran_bio_heal(state)
or self.advanced_tactics and state.has(ItemNames.CYCLONE, self.player)
)
def terran_competent_anti_air(self, state: CollectionState) -> bool:
"""
Good AA unit
:param state:
:return:
"""
return (
self.terran_competent_ground_to_air(state)
or self.terran_air_anti_air(state)
)
def welcome_to_the_jungle_requirement(self, state: CollectionState) -> bool:
"""
Welcome to the Jungle requirements - able to deal with Scouts, Void Rays, Zealots and Stalkers
:param state:
:return:
"""
return (
self.terran_common_unit(state)
and self.terran_competent_ground_to_air(state)
) or (
self.advanced_tactics
and state.has_any({ItemNames.MARINE, ItemNames.VULTURE}, self.player)
and self.terran_air_anti_air(state)
)
def terran_basic_anti_air(self, state: CollectionState) -> bool:
"""
Basic AA to deal with few air units
:param state:
:return:
"""
return (
state.has_any({
ItemNames.MISSILE_TURRET, ItemNames.THOR, ItemNames.WAR_PIGS, ItemNames.SPARTAN_COMPANY,
ItemNames.HELS_ANGELS, ItemNames.BATTLECRUISER, ItemNames.MARINE, ItemNames.WRAITH,
ItemNames.VALKYRIE, ItemNames.CYCLONE, ItemNames.WINGED_NIGHTMARES, ItemNames.BRYNHILDS
}, self.player)
or self.terran_competent_anti_air(state)
or self.advanced_tactics and state.has_any({ItemNames.GHOST, ItemNames.SPECTRE, ItemNames.WIDOW_MINE, ItemNames.LIBERATOR}, self.player)
)
def terran_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_enemy: bool = True) -> int:
"""
Ability to handle defensive missions
:param state:
:param zerg_enemy:
:param air_enemy:
:return:
"""
defense_score = sum((defense_ratings[item] for item in defense_ratings if state.has(item, self.player)))
# Manned Bunker
if state.has_any({ItemNames.MARINE, ItemNames.MARAUDER}, self.player) and state.has(ItemNames.BUNKER, self.player):
defense_score += 3
elif zerg_enemy and state.has(ItemNames.FIREBAT, self.player) and state.has(ItemNames.BUNKER, self.player):
defense_score += 2
# Siege Tank upgrades
if state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_MAELSTROM_ROUNDS}, self.player):
defense_score += 2
if state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_GRADUATING_RANGE}, self.player):
defense_score += 1
# Widow Mine upgrade
if state.has_all({ItemNames.WIDOW_MINE, ItemNames.WIDOW_MINE_CONCEALMENT}, self.player):
defense_score += 1
# Viking with splash
if state.has_all({ItemNames.VIKING, ItemNames.VIKING_SHREDDER_ROUNDS}, self.player):
defense_score += 2
# General enemy-based rules
if zerg_enemy:
defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if state.has(item, self.player)))
if air_enemy:
defense_score += sum((air_defense_ratings[item] for item in air_defense_ratings if state.has(item, self.player)))
if air_enemy and zerg_enemy and state.has(ItemNames.VALKYRIE, self.player):
# Valkyries shred mass Mutas, most common air enemy that's massed in these cases
defense_score += 2
# Advanced Tactics bumps defense rating requirements down by 2
if self.advanced_tactics:
defense_score += 2
return defense_score
def terran_competent_comp(self, state: CollectionState) -> bool:
"""
Ability to deal with most of hard missions
:param state:
:return:
"""
return (
(
(state.has_any({ItemNames.MARINE, ItemNames.MARAUDER}, self.player) and self.terran_bio_heal(state))
or state.has_any({ItemNames.THOR, ItemNames.BANSHEE, ItemNames.SIEGE_TANK}, self.player)
or state.has_all({ItemNames.LIBERATOR, ItemNames.LIBERATOR_RAID_ARTILLERY}, self.player)
)
and self.terran_competent_anti_air(state)
) or (
state.has(ItemNames.BATTLECRUISER, self.player) and self.terran_common_unit(state)
)
def great_train_robbery_train_stopper(self, state: CollectionState) -> bool:
"""
Ability to deal with trains (moving target with a lot of HP)
:param state:
:return:
"""
return (
state.has_any({ItemNames.SIEGE_TANK, ItemNames.DIAMONDBACK, ItemNames.MARAUDER, ItemNames.CYCLONE, ItemNames.BANSHEE}, self.player)
or self.advanced_tactics
and (
state.has_all({ItemNames.REAPER, ItemNames.REAPER_G4_CLUSTERBOMB}, self.player)
or state.has_all({ItemNames.SPECTRE, ItemNames.SPECTRE_PSIONIC_LASH}, self.player)
or state.has_any({ItemNames.VULTURE, ItemNames.LIBERATOR}, self.player)
)
)
def terran_can_rescue(self, state) -> bool:
"""
Rescuing in The Moebius Factor
:param state:
:return:
"""
return state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES, ItemNames.RAVEN, ItemNames.VIKING}, self.player) or self.advanced_tactics
def terran_beats_protoss_deathball(self, state: CollectionState) -> bool:
"""
Ability to deal with Immortals, Colossi with some air support
:param state:
:return:
"""
return (
(
state.has_any({ItemNames.BANSHEE, ItemNames.BATTLECRUISER}, self.player)
or state.has_all({ItemNames.LIBERATOR, ItemNames.LIBERATOR_RAID_ARTILLERY}, self.player)
) and self.terran_competent_anti_air(state)
or self.terran_competent_comp(state) and self.terran_air_anti_air(state)
)
def marine_medic_upgrade(self, state: CollectionState) -> bool:
"""
Infantry upgrade to infantry-only no-build segments
:param state:
:return:
"""
return state.has_any({
ItemNames.MARINE_COMBAT_SHIELD, ItemNames.MARINE_MAGRAIL_MUNITIONS, ItemNames.MEDIC_STABILIZER_MEDPACKS
}, self.player) \
or (state.count(ItemNames.MARINE_PROGRESSIVE_STIMPACK, self.player) >= 2
and state.has_group("Missions", self.player, 1))
def terran_survives_rip_field(self, state: CollectionState) -> bool:
"""
Ability to deal with large areas with environment damage
:param state:
:return:
"""
return (state.has(ItemNames.BATTLECRUISER, self.player)
or self.terran_air(state) and self.terran_competent_anti_air(state) and self.terran_sustainable_mech_heal(state))
def terran_sustainable_mech_heal(self, state: CollectionState) -> bool:
"""
Can heal mech units without spending resources
:param state:
:return:
"""
return state.has(ItemNames.SCIENCE_VESSEL, self.player) \
or state.has_all({ItemNames.MEDIC, ItemNames.MEDIC_ADAPTIVE_MEDPACKS}, self.player) \
or state.count(ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL, self.player) >= 3 \
or (self.advanced_tactics
and (
state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_BIO_MECHANICAL_REPAIR_DRONE}, self.player)
or state.count(ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL, self.player) >= 2)
)
def terran_bio_heal(self, state: CollectionState) -> bool:
"""
Ability to heal bio units
:param state:
:return:
"""
return state.has_any({ItemNames.MEDIC, ItemNames.MEDIVAC}, self.player) \
or self.advanced_tactics and state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_BIO_MECHANICAL_REPAIR_DRONE}, self.player)
def terran_base_trasher(self, state: CollectionState) -> bool:
"""
Can attack heavily defended bases
:param state:
:return:
"""
return state.has(ItemNames.SIEGE_TANK, self.player) \
or state.has_all({ItemNames.BATTLECRUISER, ItemNames.BATTLECRUISER_ATX_LASER_BATTERY}, self.player) \
or state.has_all({ItemNames.LIBERATOR, ItemNames.LIBERATOR_RAID_ARTILLERY}, self.player) \
or (self.advanced_tactics
and ((state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, self.player)
or self.can_nuke(state))
and (
state.has_all({ItemNames.VIKING, ItemNames.VIKING_SHREDDER_ROUNDS}, self.player)
or state.has_all({ItemNames.BANSHEE, ItemNames.BANSHEE_SHOCKWAVE_MISSILE_BATTERY}, self.player))
)
)
def terran_mobile_detector(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.RAVEN, ItemNames.SCIENCE_VESSEL, ItemNames.PROGRESSIVE_ORBITAL_COMMAND}, self.player)
def can_nuke(self, state: CollectionState) -> bool:
"""
Ability to launch nukes
:param state:
:return:
"""
return (self.advanced_tactics
and (state.has_any({ItemNames.GHOST, ItemNames.SPECTRE}, self.player)
or state.has_all({ItemNames.THOR, ItemNames.THOR_BUTTON_WITH_A_SKULL_ON_IT}, self.player)))
def terran_respond_to_colony_infestations(self, state: CollectionState) -> bool:
"""
Can deal quickly with Brood Lords and Mutas in Haven's Fall and being able to progress the mission
:param state:
:return:
"""
return (
self.terran_common_unit(state)
and self.terran_competent_anti_air(state)
and (
self.terran_air_anti_air(state)
or state.has_any({ItemNames.BATTLECRUISER, ItemNames.VALKYRIE}, self.player)
)
and self.terran_defense_rating(state, True) >= 3
)
def engine_of_destruction_requirement(self, state: CollectionState):
return self.marine_medic_upgrade(state) \
and (
self.terran_competent_anti_air(state)
and self.terran_common_unit(state) or state.has(ItemNames.WRAITH, self.player)
)
def all_in_requirement(self, state: CollectionState):
"""
All-in
:param state:
:return:
"""
beats_kerrigan = state.has_any({ItemNames.MARINE, ItemNames.BANSHEE, ItemNames.GHOST}, self.player) or self.advanced_tactics
if get_option_value(self.world, 'all_in_map') == AllInMap.option_ground:
# Ground
defense_rating = self.terran_defense_rating(state, True, False)
if state.has_any({ItemNames.BATTLECRUISER, ItemNames.BANSHEE}, self.player):
defense_rating += 2
return defense_rating >= 13 and beats_kerrigan
else:
# Air
defense_rating = self.terran_defense_rating(state, True, True)
return defense_rating >= 9 and beats_kerrigan \
and state.has_any({ItemNames.VIKING, ItemNames.BATTLECRUISER, ItemNames.VALKYRIE}, self.player) \
and state.has_any({ItemNames.HIVE_MIND_EMULATOR, ItemNames.PSI_DISRUPTER, ItemNames.MISSILE_TURRET}, self.player)
# HotS
def zerg_common_unit(self, state: CollectionState) -> bool:
return state.has_any(self.basic_zerg_units, self.player)
def zerg_competent_anti_air(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.HYDRALISK, ItemNames.MUTALISK, ItemNames.CORRUPTOR, ItemNames.BROOD_QUEEN}, self.player) \
or state.has_all({ItemNames.SWARM_HOST, ItemNames.SWARM_HOST_PRESSURIZED_GLANDS}, self.player) \
or state.has_all({ItemNames.SCOURGE, ItemNames.SCOURGE_RESOURCE_EFFICIENCY}, self.player) \
or (self.advanced_tactics and state.has(ItemNames.INFESTOR, self.player))
def zerg_basic_anti_air(self, state: CollectionState) -> bool:
return self.zerg_competent_anti_air(state) or self.kerrigan_unit_available in kerrigan_unit_available or \
state.has_any({ItemNames.SWARM_QUEEN, ItemNames.SCOURGE}, self.player) or (self.advanced_tactics and state.has(ItemNames.SPORE_CRAWLER, self.player))
def morph_brood_lord(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.MUTALISK, ItemNames.CORRUPTOR}, self.player) \
and state.has(ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, self.player)
def morph_viper(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.MUTALISK, ItemNames.CORRUPTOR}, self.player) \
and state.has(ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT, self.player)
def morph_impaler_or_lurker(self, state: CollectionState) -> bool:
return state.has(ItemNames.HYDRALISK, self.player) and state.has_any({ItemNames.HYDRALISK_IMPALER_ASPECT, ItemNames.HYDRALISK_LURKER_ASPECT}, self.player)
def zerg_competent_comp(self, state: CollectionState) -> bool:
advanced = self.advanced_tactics
core_unit = state.has_any({ItemNames.ROACH, ItemNames.ABERRATION, ItemNames.ZERGLING}, self.player)
support_unit = state.has_any({ItemNames.SWARM_QUEEN, ItemNames.HYDRALISK}, self.player) \
or self.morph_brood_lord(state) \
or advanced and (state.has_any({ItemNames.INFESTOR, ItemNames.DEFILER}, self.player) or self.morph_viper(state))
if core_unit and support_unit:
return True
vespene_unit = state.has_any({ItemNames.ULTRALISK, ItemNames.ABERRATION}, self.player) \
or advanced and self.morph_viper(state)
return vespene_unit and state.has_any({ItemNames.ZERGLING, ItemNames.SWARM_QUEEN}, self.player)
def spread_creep(self, state: CollectionState) -> bool:
return self.advanced_tactics or state.has(ItemNames.SWARM_QUEEN, self.player)
def zerg_competent_defense(self, state: CollectionState) -> bool:
return (
self.zerg_common_unit(state)
and (
(
state.has(ItemNames.SWARM_HOST, self.player)
or self.morph_brood_lord(state)
or self.morph_impaler_or_lurker(state)
) or (
self.advanced_tactics
and (self.morph_viper(state)
or state.has(ItemNames.SPINE_CRAWLER, self.player))
)
)
)
def basic_kerrigan(self, state: CollectionState) -> bool:
# One active ability that can be used to defeat enemies directly on Standard
if not self.advanced_tactics and \
not state.has_any({ItemNames.KERRIGAN_KINETIC_BLAST, ItemNames.KERRIGAN_LEAPING_STRIKE,
ItemNames.KERRIGAN_CRUSHING_GRIP, ItemNames.KERRIGAN_PSIONIC_SHIFT,
ItemNames.KERRIGAN_SPAWN_BANELINGS}, self.player):
return False
# Two non-ultimate abilities
count = 0
for item in (ItemNames.KERRIGAN_KINETIC_BLAST, ItemNames.KERRIGAN_LEAPING_STRIKE, ItemNames.KERRIGAN_HEROIC_FORTITUDE,
ItemNames.KERRIGAN_CHAIN_REACTION, ItemNames.KERRIGAN_CRUSHING_GRIP, ItemNames.KERRIGAN_PSIONIC_SHIFT,
ItemNames.KERRIGAN_SPAWN_BANELINGS, ItemNames.KERRIGAN_INFEST_BROODLINGS, ItemNames.KERRIGAN_FURY):
if state.has(item, self.player):
count += 1
if count >= 2:
return True
return False
def two_kerrigan_actives(self, state: CollectionState) -> bool:
count = 0
for i in range(7):
if state.has_any(kerrigan_actives[i], self.player):
count += 1
return count >= 2
def zerg_pass_vents(self, state: CollectionState) -> bool:
return self.story_tech_granted \
or state.has_any({ItemNames.ZERGLING, ItemNames.HYDRALISK, ItemNames.ROACH}, self.player) \
or (self.advanced_tactics and state.has(ItemNames.INFESTOR, self.player))
def supreme_requirement(self, state: CollectionState) -> bool:
return self.story_tech_granted \
or not self.kerrigan_unit_available \
or (
state.has_all({ItemNames.KERRIGAN_LEAPING_STRIKE, ItemNames.KERRIGAN_MEND}, self.player)
and self.kerrigan_levels(state, 35)
)
def kerrigan_levels(self, state: CollectionState, target: int) -> bool:
if self.story_levels_granted or not self.kerrigan_unit_available:
return True # Levels are granted
if self.kerrigan_levels_per_mission_completed > 0 \
and self.kerrigan_levels_per_mission_completed_cap > 0 \
and not self.is_item_placement(state):
# Levels can be granted from mission completion.
# Item pool filtering isn't aware of missions beaten. Assume that missions beaten will fulfill this rule.
return True
# Levels from missions beaten
levels = self.kerrigan_levels_per_mission_completed * state.count_group("Missions", self.player)
if self.kerrigan_levels_per_mission_completed_cap != -1:
levels = min(levels, self.kerrigan_levels_per_mission_completed_cap)
# Levels from items
for kerrigan_level_item in kerrigan_levels:
level_amount = get_full_item_list()[kerrigan_level_item].number
item_count = state.count(kerrigan_level_item, self.player)
levels += item_count * level_amount
# Total level cap
if self.kerrigan_total_level_cap != -1:
levels = min(levels, self.kerrigan_total_level_cap)
return levels >= target
def the_reckoning_requirement(self, state: CollectionState) -> bool:
if self.take_over_ai_allies:
return self.terran_competent_comp(state) \
and self.zerg_competent_comp(state) \
and (self.zerg_competent_anti_air(state)
or self.terran_competent_anti_air(state))
else:
return self.zerg_competent_comp(state) \
and self.zerg_competent_anti_air(state)
# LotV
def protoss_common_unit(self, state: CollectionState) -> bool:
return state.has_any(self.basic_protoss_units, self.player)
def protoss_basic_anti_air(self, state: CollectionState) -> bool:
return self.protoss_competent_anti_air(state) \
or state.has_any({ItemNames.PHOENIX, ItemNames.MIRAGE, ItemNames.CORSAIR, ItemNames.CARRIER, ItemNames.SCOUT,
ItemNames.DARK_ARCHON, ItemNames.WRATHWALKER, ItemNames.MOTHERSHIP}, self.player) \
or state.has_all({ItemNames.WARP_PRISM, ItemNames.WARP_PRISM_PHASE_BLASTER}, self.player) \
or self.advanced_tactics and state.has_any(
{ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ASCENDANT, ItemNames.DARK_TEMPLAR,
ItemNames.SENTRY, ItemNames.ENERGIZER}, self.player)
def protoss_anti_armor_anti_air(self, state: CollectionState) -> bool:
return self.protoss_competent_anti_air(state) \
or state.has_any({ItemNames.SCOUT, ItemNames.WRATHWALKER}, self.player) \
or (state.has_any({ItemNames.IMMORTAL, ItemNames.ANNIHILATOR}, self.player)
and state.has(ItemNames.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS, self.player))
def protoss_anti_light_anti_air(self, state: CollectionState) -> bool:
return self.protoss_competent_anti_air(state) \
or state.has_any({ItemNames.PHOENIX, ItemNames.MIRAGE, ItemNames.CORSAIR, ItemNames.CARRIER}, self.player)
def protoss_competent_anti_air(self, state: CollectionState) -> bool:
return state.has_any(
{ItemNames.STALKER, ItemNames.SLAYER, ItemNames.INSTIGATOR, ItemNames.DRAGOON, ItemNames.ADEPT,
ItemNames.VOID_RAY, ItemNames.DESTROYER, ItemNames.TEMPEST}, self.player) \
or (state.has_any({ItemNames.PHOENIX, ItemNames.MIRAGE, ItemNames.CORSAIR, ItemNames.CARRIER}, self.player)
and state.has_any({ItemNames.SCOUT, ItemNames.WRATHWALKER}, self.player)) \
or (self.advanced_tactics
and state.has_any({ItemNames.IMMORTAL, ItemNames.ANNIHILATOR}, self.player)
and state.has(ItemNames.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS, self.player))
def protoss_has_blink(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER}, self.player) \
or (
state.has(ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK, self.player)
and state.has_any({ItemNames.DARK_TEMPLAR, ItemNames.BLOOD_HUNTER, ItemNames.AVENGER}, self.player)
)
def protoss_can_attack_behind_chasm(self, state: CollectionState) -> bool:
return state.has_any(
{ItemNames.SCOUT, ItemNames.TEMPEST,
ItemNames.CARRIER, ItemNames.VOID_RAY, ItemNames.DESTROYER, ItemNames.MOTHERSHIP}, self.player) \
or self.protoss_has_blink(state) \
or (state.has(ItemNames.WARP_PRISM, self.player)
and (self.protoss_common_unit(state) or state.has(ItemNames.WARP_PRISM_PHASE_BLASTER, self.player))) \
or (self.advanced_tactics
and state.has_any({ItemNames.ORACLE, ItemNames.ARBITER}, self.player))
def protoss_fleet(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.CARRIER, ItemNames.TEMPEST, ItemNames.VOID_RAY, ItemNames.DESTROYER}, self.player)
def templars_return_requirement(self, state: CollectionState) -> bool:
return self.story_tech_granted \
or (
state.has_any({ItemNames.IMMORTAL, ItemNames.ANNIHILATOR}, self.player)
and state.has_any({ItemNames.COLOSSUS, ItemNames.VANGUARD, ItemNames.REAVER, ItemNames.DARK_TEMPLAR}, self.player)
and state.has_any({ItemNames.SENTRY, ItemNames.HIGH_TEMPLAR}, self.player)
)
def brothers_in_arms_requirement(self, state: CollectionState) -> bool:
return (
self.protoss_common_unit(state)
and self.protoss_anti_armor_anti_air(state)
and self.protoss_hybrid_counter(state)
) or (
self.take_over_ai_allies
and (
self.terran_common_unit(state)
or self.protoss_common_unit(state)
)
and (
self.terran_competent_anti_air(state)
or self.protoss_anti_armor_anti_air(state)
)
and (
self.protoss_hybrid_counter(state)
or state.has_any({ItemNames.BATTLECRUISER, ItemNames.LIBERATOR, ItemNames.SIEGE_TANK}, self.player)
or state.has_all({ItemNames.SPECTRE, ItemNames.SPECTRE_PSIONIC_LASH}, self.player)
or (state.has(ItemNames.IMMORTAL, self.player)
and state.has_any({ItemNames.MARINE, ItemNames.MARAUDER}, self.player)
and self.terran_bio_heal(state))
)
)
def protoss_hybrid_counter(self, state: CollectionState) -> bool:
"""
Ground Hybrids
"""
return state.has_any(
{ItemNames.ANNIHILATOR, ItemNames.ASCENDANT, ItemNames.TEMPEST, ItemNames.CARRIER, ItemNames.VOID_RAY,
ItemNames.WRATHWALKER, ItemNames.VANGUARD}, self.player) \
or (state.has(ItemNames.IMMORTAL, self.player) or self.advanced_tactics) and state.has_any(
{ItemNames.STALKER, ItemNames.DRAGOON, ItemNames.ADEPT, ItemNames.INSTIGATOR, ItemNames.SLAYER}, self.player)
def the_infinite_cycle_requirement(self, state: CollectionState) -> bool:
return self.story_tech_granted \
or not self.kerrigan_unit_available \
or (
self.two_kerrigan_actives(state)
and self.basic_kerrigan(state)
and self.kerrigan_levels(state, 70)
)
def protoss_basic_splash(self, state: CollectionState) -> bool:
return state.has_any(
{ItemNames.ZEALOT, ItemNames.COLOSSUS, ItemNames.VANGUARD, ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER,
ItemNames.DARK_TEMPLAR, ItemNames.REAVER, ItemNames.ASCENDANT}, self.player)
def protoss_static_defense(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH}, self.player)
def last_stand_requirement(self, state: CollectionState) -> bool:
return self.protoss_common_unit(state) \
and self.protoss_competent_anti_air(state) \
and self.protoss_static_defense(state) \
and (
self.advanced_tactics
or self.protoss_basic_splash(state)
)
def harbinger_of_oblivion_requirement(self, state: CollectionState) -> bool:
return self.protoss_anti_armor_anti_air(state) and (
self.take_over_ai_allies
or (
self.protoss_common_unit(state)
and self.protoss_hybrid_counter(state)
)
)
def protoss_competent_comp(self, state: CollectionState) -> bool:
return self.protoss_common_unit(state) \
and self.protoss_competent_anti_air(state) \
and self.protoss_hybrid_counter(state) \
and self.protoss_basic_splash(state)
def protoss_stalker_upgrade(self, state: CollectionState) -> bool:
return (
state.has_any(
{
ItemNames.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES,
ItemNames.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION
}, self.player)
and self.lock_any_item(state, {ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER})
)
def steps_of_the_rite_requirement(self, state: CollectionState) -> bool:
return self.protoss_competent_comp(state) \
or (
self.protoss_common_unit(state)
and self.protoss_competent_anti_air(state)
and self.protoss_static_defense(state)
)
def protoss_heal(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.CARRIER, ItemNames.SENTRY, ItemNames.SHIELD_BATTERY, ItemNames.RECONSTRUCTION_BEAM}, self.player)
def templars_charge_requirement(self, state: CollectionState) -> bool:
return self.protoss_heal(state) \
and self.protoss_anti_armor_anti_air(state) \
and (
self.protoss_fleet(state)
or (self.advanced_tactics
and self.protoss_competent_comp(state)
)
)
def the_host_requirement(self, state: CollectionState) -> bool:
return (self.protoss_fleet(state)
and self.protoss_static_defense(state)
) or (
self.protoss_competent_comp(state)
and state.has(ItemNames.SOA_TIME_STOP, self.player)
)
def salvation_requirement(self, state: CollectionState) -> bool:
return [
self.protoss_competent_comp(state),
self.protoss_fleet(state),
self.protoss_static_defense(state)
].count(True) >= 2
def into_the_void_requirement(self, state: CollectionState) -> bool:
return self.protoss_competent_comp(state) \
or (
self.take_over_ai_allies
and (
state.has(ItemNames.BATTLECRUISER, self.player)
or (
state.has(ItemNames.ULTRALISK, self.player)
and self.protoss_competent_anti_air(state)
)
)
)
def essence_of_eternity_requirement(self, state: CollectionState) -> bool:
defense_score = self.terran_defense_rating(state, False, True)
if self.take_over_ai_allies and self.protoss_static_defense(state):
defense_score += 2
return defense_score >= 10 \
and (
self.terran_competent_anti_air(state)
or self.take_over_ai_allies
and self.protoss_competent_anti_air(state)
) \
and (
state.has(ItemNames.BATTLECRUISER, self.player)
or (state.has(ItemNames.BANSHEE, self.player) and state.has_any({ItemNames.VIKING, ItemNames.VALKYRIE},
self.player))
or self.take_over_ai_allies and self.protoss_fleet(state)
) \
and state.has_any({ItemNames.SIEGE_TANK, ItemNames.LIBERATOR}, self.player)
def amons_fall_requirement(self, state: CollectionState) -> bool:
if self.take_over_ai_allies:
return (
(
state.has_any({ItemNames.BATTLECRUISER, ItemNames.CARRIER}, self.player)
)
or (state.has(ItemNames.ULTRALISK, self.player)
and self.protoss_competent_anti_air(state)
and (
state.has_any({ItemNames.LIBERATOR, ItemNames.BANSHEE, ItemNames.VALKYRIE, ItemNames.VIKING}, self.player)
or state.has_all({ItemNames.WRAITH, ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player)
or self.protoss_fleet(state)
)
and (self.terran_sustainable_mech_heal(state)
or (self.spear_of_adun_autonomously_cast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_everywhere
and state.has(ItemNames.RECONSTRUCTION_BEAM, self.player))
)
)
) \
and self.terran_competent_anti_air(state) \
and self.protoss_competent_comp(state) \
and self.zerg_competent_comp(state)
else:
return state.has(ItemNames.MUTALISK, self.player) and self.zerg_competent_comp(state)
def nova_any_weapon(self, state: CollectionState) -> bool:
return state.has_any(
{ItemNames.NOVA_C20A_CANISTER_RIFLE, ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PLASMA_RIFLE,
ItemNames.NOVA_MONOMOLECULAR_BLADE, ItemNames.NOVA_BLAZEFIRE_GUNBLADE}, self.player)
def nova_ranged_weapon(self, state: CollectionState) -> bool:
return state.has_any(
{ItemNames.NOVA_C20A_CANISTER_RIFLE, ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PLASMA_RIFLE},
self.player)
def nova_splash(self, state: CollectionState) -> bool:
return state.has_any({
ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_BLAZEFIRE_GUNBLADE, ItemNames.NOVA_PULSE_GRENADES
}, self.player) \
or self.advanced_tactics and state.has_any(
{ItemNames.NOVA_PLASMA_RIFLE, ItemNames.NOVA_MONOMOLECULAR_BLADE}, self.player)
def nova_dash(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.NOVA_MONOMOLECULAR_BLADE, ItemNames.NOVA_BLINK}, self.player)
def nova_full_stealth(self, state: CollectionState) -> bool:
return state.count(ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player) >= 2
def nova_heal(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.NOVA_ARMORED_SUIT_MODULE, ItemNames.NOVA_STIM_INFUSION}, self.player)
def nova_escape_assist(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.NOVA_BLINK, ItemNames.NOVA_HOLO_DECOY, ItemNames.NOVA_IONIC_FORCE_FIELD}, self.player)
def the_escape_stuff_granted(self) -> bool:
"""
The NCO first mission requires having too much stuff first before actually able to do anything
:return:
"""
return self.story_tech_granted \
or (self.mission_order == MissionOrder.option_vanilla and self.enabled_campaigns == {SC2Campaign.NCO})
def the_escape_first_stage_requirement(self, state: CollectionState) -> bool:
return self.the_escape_stuff_granted() \
or (self.nova_ranged_weapon(state) and (self.nova_full_stealth(state) or self.nova_heal(state)))
def the_escape_requirement(self, state: CollectionState) -> bool:
return self.the_escape_first_stage_requirement(state) \
and (self.the_escape_stuff_granted() or self.nova_splash(state))
def terran_cliffjumper(self, state: CollectionState) -> bool:
return state.has(ItemNames.REAPER, self.player) \
or state.has_all({ItemNames.GOLIATH, ItemNames.GOLIATH_JUMP_JETS}, self.player) \
or state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_JUMP_JETS}, self.player)
def terran_able_to_snipe_defiler(self, state: CollectionState) -> bool:
return state.has_all({ItemNames.NOVA_JUMP_SUIT_MODULE, ItemNames.NOVA_C20A_CANISTER_RIFLE}, self.player) \
or state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_MAELSTROM_ROUNDS, ItemNames.SIEGE_TANK_JUMP_JETS}, self.player)
def sudden_strike_requirement(self, state: CollectionState) -> bool:
return self.sudden_strike_can_reach_objectives(state) \
and self.terran_able_to_snipe_defiler(state) \
and state.has_any({ItemNames.SIEGE_TANK, ItemNames.VULTURE}, self.player) \
and self.nova_splash(state) \
and (self.terran_defense_rating(state, True, False) >= 2
or state.has(ItemNames.NOVA_JUMP_SUIT_MODULE, self.player))
def sudden_strike_can_reach_objectives(self, state: CollectionState) -> bool:
return self.terran_cliffjumper(state) \
or state.has_any({ItemNames.BANSHEE, ItemNames.VIKING}, self.player) \
or (
self.advanced_tactics
and state.has(ItemNames.MEDIVAC, self.player)
and state.has_any({ItemNames.MARINE, ItemNames.MARAUDER, ItemNames.VULTURE, ItemNames.HELLION,
ItemNames.GOLIATH}, self.player)
)
def enemy_intelligence_garrisonable_unit(self, state: CollectionState) -> bool:
"""
Has unit usable as a Garrison in Enemy Intelligence
:param state:
:return:
"""
return state.has_any(
{ItemNames.MARINE, ItemNames.REAPER, ItemNames.MARAUDER, ItemNames.GHOST, ItemNames.SPECTRE,
ItemNames.HELLION, ItemNames.GOLIATH, ItemNames.WARHOUND, ItemNames.DIAMONDBACK, ItemNames.VIKING},
self.player)
def enemy_intelligence_cliff_garrison(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.REAPER, ItemNames.VIKING, ItemNames.MEDIVAC, ItemNames.HERCULES}, self.player) \
or state.has_all({ItemNames.GOLIATH, ItemNames.GOLIATH_JUMP_JETS}, self.player) \
or self.advanced_tactics and state.has_any({ItemNames.HELS_ANGELS, ItemNames.BRYNHILDS}, self.player)
def enemy_intelligence_first_stage_requirement(self, state: CollectionState) -> bool:
return self.enemy_intelligence_garrisonable_unit(state) \
and (self.terran_competent_comp(state)
or (
self.terran_common_unit(state)
and self.terran_competent_anti_air(state)
and state.has(ItemNames.NOVA_NUKE, self.player)
)
) \
and self.terran_defense_rating(state, True, True) >= 5
def enemy_intelligence_second_stage_requirement(self, state: CollectionState) -> bool:
return self.enemy_intelligence_first_stage_requirement(state) \
and self.enemy_intelligence_cliff_garrison(state) \
and (
self.story_tech_granted
or (
self.nova_any_weapon(state)
and (
self.nova_full_stealth(state)
or (self.nova_heal(state)
and self.nova_splash(state)
and self.nova_ranged_weapon(state))
)
)
)
def enemy_intelligence_third_stage_requirement(self, state: CollectionState) -> bool:
return self.enemy_intelligence_second_stage_requirement(state) \
and (
self.story_tech_granted
or (
state.has(ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player)
and self.nova_dash(state)
)
)
def trouble_in_paradise_requirement(self, state: CollectionState) -> bool:
return self.nova_any_weapon(state) \
and self.nova_splash(state) \
and self.terran_beats_protoss_deathball(state) \
and self.terran_defense_rating(state, True, True) >= 7
def night_terrors_requirement(self, state: CollectionState) -> bool:
return self.terran_common_unit(state) \
and self.terran_competent_anti_air(state) \
and (
# These can handle the waves of infested, even volatile ones
state.has(ItemNames.SIEGE_TANK, self.player)
or state.has_all({ItemNames.VIKING, ItemNames.VIKING_SHREDDER_ROUNDS}, self.player)
or (
(
# Regular infesteds
state.has(ItemNames.FIREBAT, self.player)
or state.has_all({ItemNames.HELLION, ItemNames.HELLION_HELLBAT_ASPECT}, self.player)
or (
self.advanced_tactics
and state.has_any({ItemNames.PERDITION_TURRET, ItemNames.PLANETARY_FORTRESS}, self.player)
)
)
and self.terran_bio_heal(state)
and (
# Volatile infesteds
state.has(ItemNames.LIBERATOR, self.player)
or (
self.advanced_tactics
and state.has_any({ItemNames.HERC, ItemNames.VULTURE}, self.player)
)
)
)
)
def flashpoint_far_requirement(self, state: CollectionState) -> bool:
return self.terran_competent_comp(state) \
and self.terran_mobile_detector(state) \
and self.terran_defense_rating(state, True, False) >= 6
def enemy_shadow_tripwires_tool(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.NOVA_FLASHBANG_GRENADES, ItemNames.NOVA_BLINK, ItemNames.NOVA_DOMINATION},
self.player)
def enemy_shadow_door_unlocks_tool(self, state: CollectionState) -> bool:
return state.has_any({ItemNames.NOVA_DOMINATION, ItemNames.NOVA_BLINK, ItemNames.NOVA_JUMP_SUIT_MODULE},
self.player)
def enemy_shadow_domination(self, state: CollectionState) -> bool:
return self.story_tech_granted \
or (self.nova_ranged_weapon(state)
and (self.nova_full_stealth(state)
or state.has(ItemNames.NOVA_JUMP_SUIT_MODULE, self.player)
or (self.nova_heal(state) and self.nova_splash(state))
)
)
def enemy_shadow_first_stage(self, state: CollectionState) -> bool:
return self.enemy_shadow_domination(state) \
and (self.story_tech_granted
or ((self.nova_full_stealth(state) and self.enemy_shadow_tripwires_tool(state))
or (self.nova_heal(state) and self.nova_splash(state))
)
)
def enemy_shadow_second_stage(self, state: CollectionState) -> bool:
return self.enemy_shadow_first_stage(state) \
and (self.story_tech_granted
or self.nova_splash(state)
or self.nova_heal(state)
or self.nova_escape_assist(state)
)
def enemy_shadow_door_controls(self, state: CollectionState) -> bool:
return self.enemy_shadow_second_stage(state) \
and (self.story_tech_granted or self.enemy_shadow_door_unlocks_tool(state))
def enemy_shadow_victory(self, state: CollectionState) -> bool:
return self.enemy_shadow_door_controls(state) \
and (self.story_tech_granted or self.nova_heal(state))
def dark_skies_requirement(self, state: CollectionState) -> bool:
return self.terran_common_unit(state) \
and self.terran_beats_protoss_deathball(state) \
and self.terran_defense_rating(state, False, True) >= 8
def end_game_requirement(self, state: CollectionState) -> bool:
return self.terran_competent_comp(state) \
and self.terran_mobile_detector(state) \
and (
state.has_any({ItemNames.BATTLECRUISER, ItemNames.LIBERATOR, ItemNames.BANSHEE}, self.player)
or state.has_all({ItemNames.WRAITH, ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player)
) \
and (state.has_any({ItemNames.BATTLECRUISER, ItemNames.VIKING, ItemNames.LIBERATOR}, self.player)
or (self.advanced_tactics
and state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, self.player)
)
)
def __init__(self, world: World):
self.world: World = world
self.player = None if world is None else world.player
self.logic_level = get_option_value(world, 'required_tactics')
self.advanced_tactics = self.logic_level != RequiredTactics.option_standard
self.take_over_ai_allies = get_option_value(world, "take_over_ai_allies") == TakeOverAIAllies.option_true
self.kerrigan_unit_available = get_option_value(world, 'kerrigan_presence') in kerrigan_unit_available \
and SC2Campaign.HOTS in get_enabled_campaigns(world)
self.kerrigan_levels_per_mission_completed = get_option_value(world, "kerrigan_levels_per_mission_completed")
self.kerrigan_levels_per_mission_completed_cap = get_option_value(world, "kerrigan_levels_per_mission_completed_cap")
self.kerrigan_total_level_cap = get_option_value(world, "kerrigan_total_level_cap")
self.story_tech_granted = get_option_value(world, "grant_story_tech") == GrantStoryTech.option_true
self.story_levels_granted = get_option_value(world, "grant_story_levels") != GrantStoryLevels.option_disabled
self.basic_terran_units = get_basic_units(world, SC2Race.TERRAN)
self.basic_zerg_units = get_basic_units(world, SC2Race.ZERG)
self.basic_protoss_units = get_basic_units(world, SC2Race.PROTOSS)
self.spear_of_adun_autonomously_cast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence")
self.enabled_campaigns = get_enabled_campaigns(world)
self.mission_order = get_option_value(world, "mission_order")

28
worlds/sc2/Starcraft2.kv Normal file
View File

@ -0,0 +1,28 @@
<CampaignScroll>
scroll_type: ["content", "bars"]
bar_width: dp(12)
effect_cls: "ScrollEffect"
<MultiCampaignLayout>
cols: 1
size_hint_y: None
height: self.minimum_height + 15
padding: [5,0,dp(12),0]
<CampaignLayout>:
cols: 1
<MissionLayout>:
rows: 1
<MissionCategory>:
cols: 1
spacing: [0,5]
<MissionButton>:
text_size: self.size
markup: True
halign: 'center'
valign: 'middle'
padding: [5,0,5,0]
outline_width: 1

482
worlds/sc2/__init__.py Normal file
View File

@ -0,0 +1,482 @@
import typing
from dataclasses import fields
from typing import List, Set, Iterable, Sequence, Dict, Callable, Union
from math import floor, ceil
from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
from worlds.AutoWorld import WebWorld, World
from . import ItemNames
from .Items import StarcraftItem, filler_items, get_item_table, get_full_item_list, \
get_basic_units, ItemData, upgrade_included_names, progressive_if_nco, kerrigan_actives, kerrigan_passives, \
kerrigan_only_passives, progressive_if_ext, not_balanced_starting_units, spear_of_adun_calldowns, \
spear_of_adun_castable_passives, nova_equipment
from .ItemGroups import item_name_groups
from .Locations import get_locations, LocationType, get_location_types, get_plando_locations
from .Regions import create_regions
from .Options import get_option_value, LocationInclusion, KerriganLevelItemDistribution, \
KerriganPresence, KerriganPrimalStatus, RequiredTactics, kerrigan_unit_available, StarterUnit, SpearOfAdunPresence, \
get_enabled_campaigns, SpearOfAdunAutonomouslyCastAbilityPresence, Starcraft2Options
from .PoolFilter import filter_items, get_item_upgrades, UPGRADABLE_ITEMS, missions_in_mission_table, get_used_races
from .MissionTables import MissionInfo, SC2Campaign, lookup_name_to_mission, SC2Mission, \
SC2Race
class Starcraft2WebWorld(WebWorld):
setup = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Starcraft 2 randomizer connected to an Archipelago Multiworld",
"English",
"setup_en.md",
"setup/en",
["TheCondor", "Phaneros"]
)
tutorials = [setup]
class SC2World(World):
"""
StarCraft II is a science fiction real-time strategy video game developed and published by Blizzard Entertainment.
Play as one of three factions across four campaigns in a battle for supremacy of the Koprulu Sector.
"""
game = "Starcraft 2"
web = Starcraft2WebWorld()
data_version = 6
item_name_to_id = {name: data.code for name, data in get_full_item_list().items()}
location_name_to_id = {location.name: location.code for location in get_locations(None)}
options_dataclass = Starcraft2Options
options: Starcraft2Options
item_name_groups = item_name_groups
locked_locations: typing.List[str]
location_cache: typing.List[Location]
mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = {}
final_mission_id: int
victory_item: str
required_client_version = 0, 4, 5
def __init__(self, multiworld: MultiWorld, player: int):
super(SC2World, self).__init__(multiworld, player)
self.location_cache = []
self.locked_locations = []
def create_item(self, name: str) -> Item:
data = get_full_item_list()[name]
return StarcraftItem(name, data.classification, data.code, self.player)
def create_regions(self):
self.mission_req_table, self.final_mission_id, self.victory_item = create_regions(
self, get_locations(self), self.location_cache
)
def create_items(self):
setup_events(self.player, self.locked_locations, self.location_cache)
excluded_items = get_excluded_items(self)
starter_items = assign_starter_items(self, excluded_items, self.locked_locations, self.location_cache)
fill_resource_locations(self, self.locked_locations, self.location_cache)
pool = get_item_pool(self, self.mission_req_table, starter_items, excluded_items, self.location_cache)
fill_item_pool_with_dummy_items(self, self.locked_locations, self.location_cache, pool)
self.multiworld.itempool += pool
def set_rules(self):
self.multiworld.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player)
def get_filler_item_name(self) -> str:
return self.random.choice(filler_items)
def fill_slot_data(self):
slot_data = {}
for option_name in [field.name for field in fields(Starcraft2Options)]:
option = get_option_value(self, option_name)
if type(option) in {str, int}:
slot_data[option_name] = int(option)
slot_req_table = {}
# Serialize data
for campaign in self.mission_req_table:
slot_req_table[campaign.id] = {}
for mission in self.mission_req_table[campaign]:
slot_req_table[campaign.id][mission] = self.mission_req_table[campaign][mission]._asdict()
# Replace mission objects with mission IDs
slot_req_table[campaign.id][mission]["mission"] = slot_req_table[campaign.id][mission]["mission"].id
for index in range(len(slot_req_table[campaign.id][mission]["required_world"])):
# TODO this is a band-aid, sometimes the mission_req_table already contains dicts
# as far as I can tell it's related to having multiple vanilla mission orders
if not isinstance(slot_req_table[campaign.id][mission]["required_world"][index], dict):
slot_req_table[campaign.id][mission]["required_world"][index] = slot_req_table[campaign.id][mission]["required_world"][index]._asdict()
enabled_campaigns = get_enabled_campaigns(self)
slot_data["plando_locations"] = get_plando_locations(self)
slot_data["nova_covert_ops_only"] = (enabled_campaigns == {SC2Campaign.NCO})
slot_data["mission_req"] = slot_req_table
slot_data["final_mission"] = self.final_mission_id
slot_data["version"] = 3
if SC2Campaign.HOTS not in enabled_campaigns:
slot_data["kerrigan_presence"] = KerriganPresence.option_not_present
return slot_data
def setup_events(player: int, locked_locations: typing.List[str], location_cache: typing.List[Location]):
for location in location_cache:
if location.address is None:
item = Item(location.name, ItemClassification.progression, None, player)
locked_locations.append(location.name)
location.place_locked_item(item)
def get_excluded_items(world: World) -> Set[str]:
excluded_items: Set[str] = set(get_option_value(world, 'excluded_items'))
for item in world.multiworld.precollected_items[world.player]:
excluded_items.add(item.name)
locked_items: Set[str] = set(get_option_value(world, 'locked_items'))
# Starter items are also excluded items
starter_items: Set[str] = set(get_option_value(world, 'start_inventory'))
item_table = get_full_item_list()
soa_presence = get_option_value(world, "spear_of_adun_presence")
soa_autocast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence")
enabled_campaigns = get_enabled_campaigns(world)
# Ensure no item is both guaranteed and excluded
invalid_items = excluded_items.intersection(locked_items)
invalid_count = len(invalid_items)
# Don't count starter items that can appear multiple times
invalid_count -= len([item for item in starter_items.intersection(locked_items) if item_table[item].quantity != 1])
if invalid_count > 0:
raise Exception(f"{invalid_count} item{'s are' if invalid_count > 1 else ' is'} both locked and excluded from generation. Please adjust your excluded items and locked items.")
def smart_exclude(item_choices: Set[str], choices_to_keep: int):
expected_choices = len(item_choices)
if expected_choices == 0:
return
item_choices = set(item_choices)
starter_choices = item_choices.intersection(starter_items)
excluded_choices = item_choices.intersection(excluded_items)
item_choices.difference_update(excluded_choices)
item_choices.difference_update(locked_items)
candidates = sorted(item_choices)
exclude_amount = min(expected_choices - choices_to_keep - len(excluded_choices) + len(starter_choices), len(candidates))
if exclude_amount > 0:
excluded_items.update(world.random.sample(candidates, exclude_amount))
# Nova gear exclusion if NCO not in campaigns
if SC2Campaign.NCO not in enabled_campaigns:
excluded_items = excluded_items.union(nova_equipment)
kerrigan_presence = get_option_value(world, "kerrigan_presence")
# Exclude Primal Form item if option is not set or Kerrigan is unavailable
if get_option_value(world, "kerrigan_primal_status") != KerriganPrimalStatus.option_item or \
(kerrigan_presence in {KerriganPresence.option_not_present, KerriganPresence.option_not_present_and_no_passives}):
excluded_items.add(ItemNames.KERRIGAN_PRIMAL_FORM)
# no Kerrigan & remove all passives => remove all abilities
if kerrigan_presence == KerriganPresence.option_not_present_and_no_passives:
for tier in range(7):
smart_exclude(kerrigan_actives[tier].union(kerrigan_passives[tier]), 0)
else:
# no Kerrigan, but keep non-Kerrigan passives
if kerrigan_presence == KerriganPresence.option_not_present:
smart_exclude(kerrigan_only_passives, 0)
for tier in range(7):
smart_exclude(kerrigan_actives[tier], 0)
# SOA exclusion, other cases are handled by generic race logic
if (soa_presence == SpearOfAdunPresence.option_lotv_protoss and SC2Campaign.LOTV not in enabled_campaigns) \
or soa_presence == SpearOfAdunPresence.option_not_present:
excluded_items.update(spear_of_adun_calldowns)
if (soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_lotv_protoss \
and SC2Campaign.LOTV not in enabled_campaigns) \
or soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present:
excluded_items.update(spear_of_adun_castable_passives)
return excluded_items
def assign_starter_items(world: World, excluded_items: Set[str], locked_locations: List[str], location_cache: typing.List[Location]) -> List[Item]:
starter_items: List[Item] = []
non_local_items = get_option_value(world, "non_local_items")
starter_unit = get_option_value(world, "starter_unit")
enabled_campaigns = get_enabled_campaigns(world)
first_mission = get_first_mission(world.mission_req_table)
# Ensuring that first mission is completable
if starter_unit == StarterUnit.option_off:
starter_mission_locations = [location.name for location in location_cache
if location.parent_region.name == first_mission
and location.access_rule == Location.access_rule]
if not starter_mission_locations:
# Force early unit if first mission is impossible without one
starter_unit = StarterUnit.option_any_starter_unit
if starter_unit != StarterUnit.option_off:
first_race = lookup_name_to_mission[first_mission].race
if first_race == SC2Race.ANY:
# If the first mission is a logic-less no-build
mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = world.mission_req_table
races = get_used_races(mission_req_table, world)
races.remove(SC2Race.ANY)
if lookup_name_to_mission[first_mission].race in races:
# The campaign's race is in (At least one mission that's not logic-less no-build exists)
first_race = lookup_name_to_mission[first_mission].campaign.race
elif len(races) > 0:
# The campaign only has logic-less no-build missions. Find any other valid race
first_race = world.random.choice(list(races))
if first_race != SC2Race.ANY:
# The race of the early unit has been chosen
basic_units = get_basic_units(world, first_race)
if starter_unit == StarterUnit.option_balanced:
basic_units = basic_units.difference(not_balanced_starting_units)
if first_mission == SC2Mission.DARK_WHISPERS.mission_name:
# Special case - you don't have a logicless location but need an AA
basic_units = basic_units.difference(
{ItemNames.ZEALOT, ItemNames.CENTURION, ItemNames.SENTINEL, ItemNames.BLOOD_HUNTER,
ItemNames.AVENGER, ItemNames.IMMORTAL, ItemNames.ANNIHILATOR, ItemNames.VANGUARD})
if first_mission == SC2Mission.SUDDEN_STRIKE.mission_name:
# Special case - cliffjumpers
basic_units = {ItemNames.REAPER, ItemNames.GOLIATH, ItemNames.SIEGE_TANK, ItemNames.VIKING, ItemNames.BANSHEE}
local_basic_unit = sorted(item for item in basic_units if item not in non_local_items and item not in excluded_items)
if not local_basic_unit:
# Drop non_local_items constraint
local_basic_unit = sorted(item for item in basic_units if item not in excluded_items)
if not local_basic_unit:
raise Exception("Early Unit: At least one basic unit must be included")
unit: Item = add_starter_item(world, excluded_items, local_basic_unit)
starter_items.append(unit)
# NCO-only specific rules
if first_mission == SC2Mission.SUDDEN_STRIKE.mission_name:
support_item: Union[str, None] = None
if unit.name == ItemNames.REAPER:
support_item = ItemNames.REAPER_SPIDER_MINES
elif unit.name == ItemNames.GOLIATH:
support_item = ItemNames.GOLIATH_JUMP_JETS
elif unit.name == ItemNames.SIEGE_TANK:
support_item = ItemNames.SIEGE_TANK_JUMP_JETS
elif unit.name == ItemNames.VIKING:
support_item = ItemNames.VIKING_SMART_SERVOS
if support_item is not None:
starter_items.append(add_starter_item(world, excluded_items, [support_item]))
starter_items.append(add_starter_item(world, excluded_items, [ItemNames.NOVA_JUMP_SUIT_MODULE]))
starter_items.append(
add_starter_item(world, excluded_items,
[
ItemNames.NOVA_HELLFIRE_SHOTGUN,
ItemNames.NOVA_PLASMA_RIFLE,
ItemNames.NOVA_PULSE_GRENADES
]))
if enabled_campaigns == {SC2Campaign.NCO}:
starter_items.append(add_starter_item(world, excluded_items, [ItemNames.LIBERATOR_RAID_ARTILLERY]))
starter_abilities = get_option_value(world, 'start_primary_abilities')
assert isinstance(starter_abilities, int)
if starter_abilities:
ability_count = starter_abilities
ability_tiers = [0, 1, 3]
world.random.shuffle(ability_tiers)
if ability_count > 3:
ability_tiers.append(6)
for tier in ability_tiers:
abilities = kerrigan_actives[tier].union(kerrigan_passives[tier]).difference(excluded_items, non_local_items)
if not abilities:
abilities = kerrigan_actives[tier].union(kerrigan_passives[tier]).difference(excluded_items)
if abilities:
ability_count -= 1
starter_items.append(add_starter_item(world, excluded_items, list(abilities)))
if ability_count == 0:
break
return starter_items
def get_first_mission(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]]) -> str:
# The first world should also be the starting world
campaigns = mission_req_table.keys()
lowest_id = min([campaign.id for campaign in campaigns])
first_campaign = [campaign for campaign in campaigns if campaign.id == lowest_id][0]
first_mission = list(mission_req_table[first_campaign])[0]
return first_mission
def add_starter_item(world: World, excluded_items: Set[str], item_list: Sequence[str]) -> Item:
item_name = world.random.choice(sorted(item_list))
excluded_items.add(item_name)
item = create_item_with_correct_settings(world.player, item_name)
world.multiworld.push_precollected(item)
return item
def get_item_pool(world: World, mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]],
starter_items: List[Item], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
pool: List[Item] = []
# For the future: goal items like Artifact Shards go here
locked_items = []
# YAML items
yaml_locked_items = get_option_value(world, 'locked_items')
assert not isinstance(yaml_locked_items, int)
# Adjust generic upgrade availability based on options
include_upgrades = get_option_value(world, 'generic_upgrade_missions') == 0
upgrade_items = get_option_value(world, 'generic_upgrade_items')
assert isinstance(upgrade_items, int)
# Include items from outside main campaigns
item_sets = {'wol', 'hots', 'lotv'}
if get_option_value(world, 'nco_items') \
or SC2Campaign.NCO in get_enabled_campaigns(world):
item_sets.add('nco')
if get_option_value(world, 'bw_items'):
item_sets.add('bw')
if get_option_value(world, 'ext_items'):
item_sets.add('ext')
def allowed_quantity(name: str, data: ItemData) -> int:
if name in excluded_items \
or data.type == "Upgrade" and (not include_upgrades or name not in upgrade_included_names[upgrade_items]) \
or not data.origin.intersection(item_sets):
return 0
elif name in progressive_if_nco and 'nco' not in item_sets:
return 1
elif name in progressive_if_ext and 'ext' not in item_sets:
return 1
else:
return data.quantity
for name, data in get_item_table().items():
for _ in range(allowed_quantity(name, data)):
item = create_item_with_correct_settings(world.player, name)
if name in yaml_locked_items:
locked_items.append(item)
else:
pool.append(item)
existing_items = starter_items + [item for item in world.multiworld.precollected_items[world.player] if item not in starter_items]
existing_names = [item.name for item in existing_items]
# Check the parent item integrity, exclude items
pool[:] = [item for item in pool if pool_contains_parent(item, pool + locked_items + existing_items)]
# Removing upgrades for excluded items
for item_name in excluded_items:
if item_name in existing_names:
continue
invalid_upgrades = get_item_upgrades(pool, item_name)
for invalid_upgrade in invalid_upgrades:
pool.remove(invalid_upgrade)
fill_pool_with_kerrigan_levels(world, pool)
filtered_pool = filter_items(world, mission_req_table, location_cache, pool, existing_items, locked_items)
return filtered_pool
def fill_item_pool_with_dummy_items(self: SC2World, locked_locations: List[str],
location_cache: List[Location], pool: List[Item]):
for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
item = create_item_with_correct_settings(self.player, self.get_filler_item_name())
pool.append(item)
def create_item_with_correct_settings(player: int, name: str) -> Item:
data = get_full_item_list()[name]
item = Item(name, data.classification, data.code, player)
return item
def pool_contains_parent(item: Item, pool: Iterable[Item]):
item_data = get_full_item_list().get(item.name)
if item_data.parent_item is None:
# The item has not associated parent, the item is valid
return True
parent_item = item_data.parent_item
# Check if the pool contains the parent item
return parent_item in [pool_item.name for pool_item in pool]
def fill_resource_locations(world: World, locked_locations: List[str], location_cache: List[Location]):
"""
Filters the locations in the world using a trash or Nothing item
:param multiworld:
:param player:
:param locked_locations:
:param location_cache:
:return:
"""
open_locations = [location for location in location_cache if location.item is None]
plando_locations = get_plando_locations(world)
resource_location_types = get_location_types(world, LocationInclusion.option_resources)
location_data = {sc2_location.name: sc2_location for sc2_location in get_locations(world)}
for location in open_locations:
# Go through the locations that aren't locked yet (early unit, etc)
if location.name not in plando_locations:
# The location is not plando'd
sc2_location = location_data[location.name]
if sc2_location.type in resource_location_types:
item_name = world.random.choice(filler_items)
item = create_item_with_correct_settings(world.player, item_name)
location.place_locked_item(item)
locked_locations.append(location.name)
def place_exclusion_item(item_name, location, locked_locations, player):
item = create_item_with_correct_settings(player, item_name)
location.place_locked_item(item)
locked_locations.append(location.name)
def fill_pool_with_kerrigan_levels(world: World, item_pool: List[Item]):
total_levels = get_option_value(world, "kerrigan_level_item_sum")
if get_option_value(world, "kerrigan_presence") not in kerrigan_unit_available \
or total_levels == 0 \
or SC2Campaign.HOTS not in get_enabled_campaigns(world):
return
def add_kerrigan_level_items(level_amount: int, item_amount: int):
name = f"{level_amount} Kerrigan Level"
if level_amount > 1:
name += "s"
for _ in range(item_amount):
item_pool.append(create_item_with_correct_settings(world.player, name))
sizes = [70, 35, 14, 10, 7, 5, 2, 1]
option = get_option_value(world, "kerrigan_level_item_distribution")
assert isinstance(option, int)
assert isinstance(total_levels, int)
if option in (KerriganLevelItemDistribution.option_vanilla, KerriganLevelItemDistribution.option_smooth):
distribution = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
if option == KerriganLevelItemDistribution.option_vanilla:
distribution = [32, 0, 0, 1, 3, 0, 0, 0, 1, 1]
else: # Smooth
distribution = [0, 0, 0, 1, 1, 2, 2, 2, 1, 1]
for tier in range(len(distribution)):
add_kerrigan_level_items(tier + 1, distribution[tier])
else:
size = sizes[option - 2]
round_func: Callable[[float], int] = round
if total_levels > 70:
round_func = floor
else:
round_func = ceil
add_kerrigan_level_items(size, round_func(float(total_levels) / size))

View File

@ -0,0 +1,42 @@
# Contributors
Contibutors are listed with preferred or Discord names first, with github usernames prepended with an `@`
## Update 2024.0
### Code Changes
* Ziktofel (@Ziktofel)
* Salzkorn (@Salzkorn)
* EnvyDragon (@EnvyDragon)
* Phanerus (@MatthewMarinets)
* Madi Sylveon (@MadiMadsen)
* Magnemania (@Magnemania)
* Subsourian (@Subsourian)
* Hopop (@hopop201)
* Alice Voltaire (@AliceVoltaire)
* Genderdruid (@ArchonofFail)
* CrazedCollie (@FoxOfWar)
### Additional Beta testing and bug reports
* Varcklen (@Varcklen)
* BicolourSnake (@Bicoloursnake)
* @NobleXenon
* Severencir (@Severencir)
* neocerber (@neocerber)
* Mati (@Matiya-star)
* Ixzine
* sweetox
* 8thDaughterOfFrost
* The M8
* Berserker (@Berserker66)
* KaitoKid
* Sheen
* ProfBytes
* IncoherentOrange
* eudaimonistic
* Figment
## Older versions
Not all contributors to older versions of Archipelago Starcraft 2 are known.
TheCondor (@TheCondor07) is the original maintainer of the project. Other known contributors include:
* soldieroforder
* Berserker (@Berserker66)

View File

@ -0,0 +1,64 @@
# Starcraft 2 Wings of Liberty
## What does randomization do to this game?
The following unlocks are randomized as items:
1. Your ability to build any non-worker unit.
2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss!
3. Your ability to get the generic unit upgrades, such as attack and armour upgrades.
4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades for Zerg, and Spear of Adun upgrades for Protoss.
5. Small boosts to your starting mineral, vespene gas, and supply totals on each mission.
You find items by making progress in these categories:
* Completing missions
* Completing bonus objectives (like by gathering lab research material in Wings of Liberty)
* Reaching milestones in the mission, such as completing part of a main objective
* Completing challenges based on achievements in the base game, such as clearing all Zerg on Devil's Playground
Except for mission completion, these categories can be disabled in the game's settings. For instance, you can disable getting items for reaching required milestones.
When you receive items, they will immediately become available, even during a mission, and you will be
notified via a text box in the top-right corner of the game screen. Item unlocks are also logged in the Archipelago client.
Missions are launched through the Starcraft 2 Archipelago client, through the Starcraft 2 Launcher tab. The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included. Additionally, metaprogression currencies such as credits and Solarite are not used.
## What is the goal of this game when randomized?
The goal is to beat the final mission in the mission order. The yaml configuration file controls the mission order and how missions are shuffled.
## What non-randomized changes are there from vanilla Starcraft 2?
1. Some missions have more vespene geysers available to allow a wider variety of units.
2. Many new units and upgrades have been added as items, coming from co-op, melee, later campaigns, later expansions, brood war, and original ideas.
3. Higher-tech production structures, including Factories, Starports, Robotics Facilities, and Stargates, no longer have tech requirements.
4. Zerg missions have been adjusted to give the player a starting Lair where they would only have Hatcheries.
5. Upgrades with a downside have had the downside removed, such as automated refineries costing more or tech reactors taking longer to build.
6. Unit collision within the vents in Enemy Within has been adjusted to allow larger units to travel through them without getting stuck in odd places.
7. Several vanilla bugs have been fixed.
## Which of my items can be in another player's world?
By default, any of StarCraft 2's items (specified above) can be in another player's world. See the
[Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en)
for more information on how to change this.
## Unique Local Commands
The following commands are only available when using the Starcraft 2 Client to play with Archipelago. You can list them any time in the client with `/help`.
* `/download_data` Download the most recent release of the necessary files for playing SC2 with Archipelago. Will overwrite existing files
* `/difficulty [difficulty]` Overrides the difficulty set for the world.
* Options: casual, normal, hard, brutal
* `/game_speed [game_speed]` Overrides the game speed for the world
* Options: default, slower, slow, normal, fast, faster
* `/color [faction] [color]` Changes your color for one of your playable factions.
* Faction options: raynor, kerrigan, primal, protoss, nova
* Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown, lightgreen, darkgrey, pink, rainbow, random, default
* `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation.
* Run without arguments to list all options.
* Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource amounts, controlling AI allies, etc.
* `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play the next mission in a chain the other player is doing.
* `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided
* `/available` Get what missions are currently available to play
* `/unfinished` Get what missions are currently available to play and have not had all locations checked
* `/set_path [path]` Manually set the SC2 install directory (if the automatic detection fails)

View File

@ -7,12 +7,10 @@ to obtain a config file for StarCraft 2.
- [StarCraft 2](https://starcraft2.com/en-us/)
- [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
- [StarCraft 2 AP Maps and Data](https://github.com/Ziktofel/Archipelago-SC2-data/releases)
## How do I install this randomizer?
1. Install StarCraft 2 and Archipelago using the first two links above. (The StarCraft 2 client for Archipelago is
included by default.)
1. Install StarCraft 2 and Archipelago using the links above. The StarCraft 2 Archipelago client is downloaded by the Archipelago installer.
- Linux users should also follow the instructions found at the bottom of this page
(["Running in Linux"](#running-in-linux)).
2. Run ArchipelagoStarcraft2Client.exe.
@ -21,25 +19,66 @@ to obtain a config file for StarCraft 2.
## Where do I get a config file (aka "YAML") for this game?
The [Player Settings](https://archipelago.gg/games/Starcraft%202%20Wings%20of%20Liberty/player-settings) page on this
website allows you to choose your personal settings for the randomizer and download them into a config file. Remember
the name you type in the `Player Name` box; that's the "slot name" the client will ask you for when you attempt to
connect!
Yaml files are configuration files that tell Archipelago how you'd like your game to be randomized, even if you're only using default options.
When you're setting up a multiworld, every world needs its own yaml file.
### And why do I need a config file?
There are three basic ways to get a yaml:
* You can go to the [Player Options](https://archipelago.gg/games/Starcraft%202/player-options) page, set your options in the GUI, and export the yaml.
* You can generate a template, either by downloading it from the [Player Options](https://archipelago.gg/games/Starcraft%202/player-options) page or by generating it from the Launcher (ArchipelagoLauncher.exe). The template includes descriptions of each option, you just have to edit it in your text editor of choice.
* You can ask someone else to share their yaml to use it for yourself or adjust it as you wish.
Config files tell Archipelago how you'd like your game to be randomized, even if you're only using default settings.
When you're setting up a multiworld, every world needs its own config file.
Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en#creating-a-yaml) for more information.
Remember the name you enter in the options page or in the yaml file, you'll need it to connect later!
Note that the basic Player Options page doesn't allow you to change all advanced options, such as excluding particular units or upgrades. Go through the [Weighted Options](https://archipelago.gg/weighted-options) page for that.
Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en#creating-a-yaml) for more game-agnostic information.
### Common yaml questions
#### How do I know I set my yaml up correctly?
The simplest way to check is to test it out. Save your yaml to the Players/ folder within your Archipelago installation and run ArchipelagoGenerate.exe. You should see a new .zip file within the output/ folder of your Archipelago installation if things worked correctly. It's advisable to run ArchipelagoGenerate through a terminal so that you can see the printout, which will include any errors and the precise output file name if it's successful. If you don't like terminals, you can also check the log file in the logs/ folder.
#### What does Progression Balancing do?
For Starcraft 2, not much. It's an Archipelago-wide option meant to shift required items earlier in the playthrough, but Starcraft 2 tends to be much more open in what items you can use. As such, this adjustment isn't very noticeable. It can also increase generation times, so we generally recommend turning it off.
#### How do I specify items in a list, like in excluded items?
You can look up the syntax for yaml collections in the [YAML specification](https://yaml.org/spec/1.2.2/#21-collections). For lists, every item goes on its own line, started with a hyphen:
```yaml
excluded_items:
- Battlecruiser
- Drop-Pods (Kerrigan Tier 7)
```
An empty list is just a matching pair of square brackets: `[]`. That's the default value in the template, which should let you know to use this syntax.
#### How do I specify items for the starting inventory?
The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with. The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value:
```yaml
start_inventory:
Micro-Filtering: 1
Additional Starting Vespene: 5
```
An empty mapping is just a matching pair of curly braces: `{}`. That's the default value in the template, which should let you know to use this syntax.
#### How do I know the exact names of items?
You can look up a complete list if item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/).
## How do I join a MultiWorld game?
1. Run ArchipelagoStarcraft2Client.exe.
- macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only.
2. Type `/connect [server ip]`.
3. Type your slot name and the server's password when prompted.
4. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see every mission. By default,
only 'Liberation Day' will be available at the beginning. Just click on a mission to start it!
- If you're running through the website, the server IP should be displayed near the top of the room page.
3. Type your slot name from your YAML when prompted.
4. If the server has a password, enter that when prompted.
5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your world. Unreachable missions will have greyed-out text. Just click on an available mission to start it!
## The game isn't launching when I try to start a mission.

View File

View File

@ -0,0 +1,41 @@
import unittest
from .test_base import Sc2TestBase
from .. import Regions
from .. import Options, MissionTables
class TestGridsizes(unittest.TestCase):
def test_grid_sizes_meet_specs(self):
self.assertTupleEqual((1, 2, 0), Regions.get_grid_dimensions(2))
self.assertTupleEqual((1, 3, 0), Regions.get_grid_dimensions(3))
self.assertTupleEqual((2, 2, 0), Regions.get_grid_dimensions(4))
self.assertTupleEqual((2, 3, 1), Regions.get_grid_dimensions(5))
self.assertTupleEqual((2, 4, 1), Regions.get_grid_dimensions(7))
self.assertTupleEqual((2, 4, 0), Regions.get_grid_dimensions(8))
self.assertTupleEqual((3, 3, 0), Regions.get_grid_dimensions(9))
self.assertTupleEqual((2, 5, 0), Regions.get_grid_dimensions(10))
self.assertTupleEqual((3, 4, 1), Regions.get_grid_dimensions(11))
self.assertTupleEqual((3, 4, 0), Regions.get_grid_dimensions(12))
self.assertTupleEqual((3, 5, 0), Regions.get_grid_dimensions(15))
self.assertTupleEqual((4, 4, 0), Regions.get_grid_dimensions(16))
self.assertTupleEqual((4, 6, 0), Regions.get_grid_dimensions(24))
self.assertTupleEqual((5, 5, 0), Regions.get_grid_dimensions(25))
self.assertTupleEqual((5, 6, 1), Regions.get_grid_dimensions(29))
self.assertTupleEqual((5, 7, 2), Regions.get_grid_dimensions(33))
class TestGridGeneration(Sc2TestBase):
options = {
"mission_order": Options.MissionOrder.option_grid,
"excluded_missions": [MissionTables.SC2Mission.ZERO_HOUR.mission_name,],
"enable_hots_missions": False,
"enable_prophecy_missions": True,
"enable_lotv_prologue_missions": False,
"enable_lotv_missions": False,
"enable_epilogue_missions": False,
"enable_nco_missions": False
}
def test_size_matches_exclusions(self):
self.assertNotIn(MissionTables.SC2Mission.ZERO_HOUR.mission_name, self.multiworld.regions)
# WoL has 29 missions. -1 for Zero Hour being excluded, +1 for the automatically-added menu location
self.assertEqual(len(self.multiworld.regions), 29)

View File

@ -0,0 +1,11 @@
from typing import *
from test.TestBase import WorldTestBase
from .. import SC2World
from .. import Client
class Sc2TestBase(WorldTestBase):
game = Client.SC2Context.game
world: SC2World
player: ClassVar[int] = 1
skip_long_tests: bool = True

View File

@ -0,0 +1,7 @@
import unittest
from .test_base import Sc2TestBase
from .. import Options, MissionTables
class TestOptions(unittest.TestCase):
def test_campaign_size_option_max_matches_number_of_missions(self):
self.assertEqual(Options.MaximumCampaignSize.range_end, len(MissionTables.SC2Mission))

File diff suppressed because it is too large Load Diff

View File

@ -1,421 +0,0 @@
from BaseClasses import Item, ItemClassification, MultiWorld
import typing
from .Options import get_option_value
from .MissionTables import vanilla_mission_req_table
class ItemData(typing.NamedTuple):
code: typing.Optional[int]
type: typing.Optional[str]
number: typing.Optional[int]
classification: ItemClassification = ItemClassification.useful
quantity: int = 1
parent_item: str = None
origin: typing.Set[str] = {"wol"}
class StarcraftWoLItem(Item):
game: str = "Starcraft 2 Wings of Liberty"
def get_full_item_list():
return item_table
SC2WOL_ITEM_ID_OFFSET = 1000
item_table = {
"Marine": ItemData(0 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, classification=ItemClassification.progression),
"Medic": ItemData(1 + SC2WOL_ITEM_ID_OFFSET, "Unit", 1, classification=ItemClassification.progression),
"Firebat": ItemData(2 + SC2WOL_ITEM_ID_OFFSET, "Unit", 2, classification=ItemClassification.progression),
"Marauder": ItemData(3 + SC2WOL_ITEM_ID_OFFSET, "Unit", 3, classification=ItemClassification.progression),
"Reaper": ItemData(4 + SC2WOL_ITEM_ID_OFFSET, "Unit", 4, classification=ItemClassification.progression),
"Hellion": ItemData(5 + SC2WOL_ITEM_ID_OFFSET, "Unit", 5, classification=ItemClassification.progression),
"Vulture": ItemData(6 + SC2WOL_ITEM_ID_OFFSET, "Unit", 6, classification=ItemClassification.progression),
"Goliath": ItemData(7 + SC2WOL_ITEM_ID_OFFSET, "Unit", 7, classification=ItemClassification.progression),
"Diamondback": ItemData(8 + SC2WOL_ITEM_ID_OFFSET, "Unit", 8, classification=ItemClassification.progression),
"Siege Tank": ItemData(9 + SC2WOL_ITEM_ID_OFFSET, "Unit", 9, classification=ItemClassification.progression),
"Medivac": ItemData(10 + SC2WOL_ITEM_ID_OFFSET, "Unit", 10, classification=ItemClassification.progression),
"Wraith": ItemData(11 + SC2WOL_ITEM_ID_OFFSET, "Unit", 11, classification=ItemClassification.progression),
"Viking": ItemData(12 + SC2WOL_ITEM_ID_OFFSET, "Unit", 12, classification=ItemClassification.progression),
"Banshee": ItemData(13 + SC2WOL_ITEM_ID_OFFSET, "Unit", 13, classification=ItemClassification.progression),
"Battlecruiser": ItemData(14 + SC2WOL_ITEM_ID_OFFSET, "Unit", 14, classification=ItemClassification.progression),
"Ghost": ItemData(15 + SC2WOL_ITEM_ID_OFFSET, "Unit", 15, classification=ItemClassification.progression),
"Spectre": ItemData(16 + SC2WOL_ITEM_ID_OFFSET, "Unit", 16, classification=ItemClassification.progression),
"Thor": ItemData(17 + SC2WOL_ITEM_ID_OFFSET, "Unit", 17, classification=ItemClassification.progression),
# EE units
"Liberator": ItemData(18 + SC2WOL_ITEM_ID_OFFSET, "Unit", 18, classification=ItemClassification.progression, origin={"nco", "ext"}),
"Valkyrie": ItemData(19 + SC2WOL_ITEM_ID_OFFSET, "Unit", 19, classification=ItemClassification.progression, origin={"bw"}),
"Widow Mine": ItemData(20 + SC2WOL_ITEM_ID_OFFSET, "Unit", 20, classification=ItemClassification.progression, origin={"ext"}),
"Cyclone": ItemData(21 + SC2WOL_ITEM_ID_OFFSET, "Unit", 21, classification=ItemClassification.progression, origin={"ext"}),
# Some other items are moved to Upgrade group because of the way how the bot message is parsed
"Progressive Infantry Weapon": ItemData(100 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3),
"Progressive Infantry Armor": ItemData(102 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3),
"Progressive Vehicle Weapon": ItemData(103 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3),
"Progressive Vehicle Armor": ItemData(104 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 6, quantity=3),
"Progressive Ship Weapon": ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3),
"Progressive Ship Armor": ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3),
# Upgrade bundle 'number' values are used as indices to get affected 'number's
"Progressive Weapon Upgrade": ItemData(107 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3),
"Progressive Armor Upgrade": ItemData(108 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 1, quantity=3),
"Progressive Infantry Upgrade": ItemData(109 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3),
"Progressive Vehicle Upgrade": ItemData(110 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 3, quantity=3),
"Progressive Ship Upgrade": ItemData(111 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3),
"Progressive Weapon/Armor Upgrade": ItemData(112 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 5, quantity=3),
"Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, parent_item="Bunker"),
"Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, parent_item="Bunker"),
"Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler, parent_item="Missile Turret"),
"Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"),
"Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4),
"Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5),
"Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6),
"Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7),
"Progressive Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 0, parent_item="Marine", quantity=2),
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"),
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.filler, parent_item="Medic"),
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"),
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler, parent_item="Firebat"),
"Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13, parent_item="Firebat"),
"Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14, parent_item="Marauder"),
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, parent_item="Marauder"),
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, parent_item="Reaper"),
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression, parent_item="Reaper"),
# Items from EE
"Mag-Field Accelerators (Cyclone)": ItemData(218 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 18, parent_item="Cyclone", origin={"ext"}),
"Mag-Field Launchers (Cyclone)": ItemData(219 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 19, parent_item="Cyclone", origin={"ext"}),
# Items from new mod
"Laser Targeting System (Marine)": ItemData(220 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, classification=ItemClassification.filler, parent_item="Marine", origin={"nco"}), # Freed slot from Stimpack
"Magrail Munitions (Marine)": ItemData(221 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 20, parent_item="Marine", origin={"nco"}),
"Optimized Logistics (Marine)": ItemData(222 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 21, classification=ItemClassification.filler, parent_item="Marine", origin={"nco"}),
"Restoration (Medic)": ItemData(223 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 22, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
"Optical Flare (Medic)": ItemData(224 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 23, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
"Optimized Logistics (Medic)": ItemData(225 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 24, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
"Progressive Stimpack (Firebat)": ItemData(226 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 6, parent_item="Firebat", quantity=2, origin={"bw"}),
"Optimized Logistics (Firebat)": ItemData(227 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 25, parent_item="Firebat", origin={"bw"}),
"Progressive Stimpack (Marauder)": ItemData(228 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 8, parent_item="Marauder", quantity=2, origin={"nco"}),
"Laser Targeting System (Marauder)": ItemData(229 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 26, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
"Magrail Munitions (Marauder)": ItemData(230 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 27, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
"Internal Tech Module (Marauder)": ItemData(231 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 28, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
# Items from new mod
"Progressive Stimpack (Reaper)": ItemData(250 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 10, parent_item="Reaper", quantity=2, origin={"nco"}),
"Laser Targeting System (Reaper)": ItemData(251 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 0, classification=ItemClassification.filler, parent_item="Reaper", origin={"nco"}),
"Advanced Cloaking Field (Reaper)": ItemData(252 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 1, parent_item="Reaper", origin={"nco"}),
"Spider Mines (Reaper)": ItemData(253 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 2, classification=ItemClassification.filler, parent_item="Reaper", origin={"nco"}),
"Combat Drugs (Reaper)": ItemData(254 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 3, classification=ItemClassification.filler, parent_item="Reaper", origin={"ext"}),
"Hellbat Aspect (Hellion)": ItemData(255 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 4, parent_item="Hellion", origin={"nco"}),
"Smart Servos (Hellion)": ItemData(256 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 5, parent_item="Hellion", origin={"nco"}),
"Optimized Logistics (Hellion)": ItemData(257 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 6, classification=ItemClassification.filler, parent_item="Hellion", origin={"nco"}),
"Jump Jets (Hellion)": ItemData(258 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 7, classification=ItemClassification.filler, parent_item="Hellion", origin={"nco"}),
"Progressive Stimpack (Hellion)": ItemData(259 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 12, parent_item="Hellion", quantity=2, origin={"nco"}),
"Ion Thrusters (Vulture)": ItemData(260 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 8, classification=ItemClassification.filler, parent_item="Vulture", origin={"bw"}),
"Auto Launchers (Vulture)": ItemData(261 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 9, parent_item="Vulture", origin={"bw"}),
"High Explosive Munition (Spider Mine)": ItemData(262 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 10, origin={"bw"}),
"Jump Jets (Goliath)": ItemData(263 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 11, classification=ItemClassification.filler, parent_item="Goliath", origin={"nco"}),
"Optimized Logistics (Goliath)": ItemData(264 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 12, classification=ItemClassification.filler, parent_item="Goliath", origin={"nco"}),
"Hyperfluxor (Diamondback)": ItemData(265 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 13, parent_item="Diamondback", origin={"ext"}),
"Burst Capacitors (Diamondback)": ItemData(266 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 14, classification=ItemClassification.filler, parent_item="Diamondback", origin={"ext"}),
"Optimized Logistics (Diamondback)": ItemData(267 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 15, parent_item="Diamondback", origin={"ext"}),
"Jump Jets (Siege Tank)": ItemData(268 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 16, parent_item="Siege Tank", origin={"nco"}),
"Spider Mines (Siege Tank)": ItemData(269 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 17, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
"Smart Servos (Siege Tank)": ItemData(270 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 18, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
"Graduating Range (Siege Tank)": ItemData(271 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 19, classification=ItemClassification.progression, parent_item="Siege Tank", origin={"ext"}),
"Laser Targeting System (Siege Tank)": ItemData(272 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 20, parent_item="Siege Tank", origin={"nco"}),
"Advanced Siege Tech (Siege Tank)": ItemData(273 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 21, parent_item="Siege Tank", origin={"ext"}),
"Internal Tech Module (Siege Tank)": ItemData(274 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 22, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
"Optimized Logistics (Predator)": ItemData(275 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 23, classification=ItemClassification.filler, parent_item="Predator", origin={"ext"}),
"Expanded Hull (Medivac)": ItemData(276 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 24, classification=ItemClassification.filler, parent_item="Medivac", origin={"ext"}),
"Afterburners (Medivac)": ItemData(277 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 25, classification=ItemClassification.filler, parent_item="Medivac", origin={"ext"}),
"Advanced Laser Technology (Wraith)": ItemData(278 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 26, classification=ItemClassification.progression, parent_item="Wraith", origin={"ext"}),
"Smart Servos (Viking)": ItemData(279 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 27, parent_item="Viking", origin={"ext"}),
"Magrail Munitions (Viking)": ItemData(280 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 28, parent_item="Viking", origin={"ext"}),
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler, parent_item="Hellion"),
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, parent_item="Hellion"),
"Cerberus Mine (Spider Mine)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler),
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler, parent_item="Vulture"),
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, parent_item="Goliath"),
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, parent_item="Goliath"),
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler, parent_item="Diamondback"),
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler, parent_item="Diamondback"),
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8, classification=ItemClassification.progression, parent_item="Siege Tank"),
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9, parent_item="Siege Tank"),
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler, parent_item="Medivac"),
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler, parent_item="Medivac"),
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler, parent_item="Wraith"),
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler, parent_item="Wraith"),
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, parent_item="Viking"),
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, parent_item="Viking"),
"Progressive Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 2, classification=ItemClassification.filler, parent_item="Banshee", quantity=2),
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, parent_item="Banshee"),
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler, parent_item="Battlecruiser"),
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler, parent_item="Battlecruiser"),
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20, parent_item="Ghost"),
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21, parent_item="Ghost"),
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression, parent_item="Spectre"),
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, parent_item="Spectre"),
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler, parent_item="Thor"),
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler, parent_item="Thor"),
# Items from EE
"Advanced Ballistics (Liberator)": ItemData(326 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 26, parent_item="Liberator", origin={"ext"}),
"Raid Artillery (Liberator)": ItemData(327 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 27, classification=ItemClassification.progression, parent_item="Liberator", origin={"nco"}),
"Drilling Claws (Widow Mine)": ItemData(328 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 28, classification=ItemClassification.filler, parent_item="Widow Mine", origin={"ext"}),
"Concealment (Widow Mine)": ItemData(329 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 29, classification=ItemClassification.progression, parent_item="Widow Mine", origin={"ext"}),
#Items from new mod
"Hyperflight Rotors (Banshee)": ItemData(350 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 0, classification=ItemClassification.filler, parent_item="Banshee", origin={"ext"}),
"Laser Targeting System (Banshee)": ItemData(351 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 1, classification=ItemClassification.filler, parent_item="Banshee", origin={"nco"}),
"Internal Tech Module (Banshee)": ItemData(352 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 2, classification=ItemClassification.filler, parent_item="Banshee", origin={"nco"}),
"Tactical Jump (Battlecruiser)": ItemData(353 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 3, parent_item="Battlecruiser", origin={"nco", "ext"}),
"Cloak (Battlecruiser)": ItemData(354 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 4, parent_item="Battlecruiser", origin={"nco"}),
"ATX Laser Battery (Battlecruiser)": ItemData(355 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 5, classification=ItemClassification.progression, parent_item="Battlecruiser", origin={"nco"}),
"Optimized Logistics (Battlecruiser)": ItemData(356 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 6, classification=ItemClassification.filler, parent_item="Battlecruiser", origin={"ext"}),
"Internal Tech Module (Battlecruiser)": ItemData(357 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 7, classification=ItemClassification.filler, parent_item="Battlecruiser", origin={"nco"}),
"EMP Rounds (Ghost)": ItemData(358 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 8, parent_item="Ghost", origin={"ext"}),
"Lockdown (Ghost)": ItemData(359 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 9, parent_item="Ghost", origin={"bw"}),
"Impaler Rounds (Spectre)": ItemData(360 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 10, parent_item="Spectre", origin={"ext"}),
"Progressive High Impact Payload (Thor)": ItemData(361 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 14, parent_item="Thor", quantity=2, origin={"ext"}), # L2 is Smart Servos
"Bio Mechanical Repair Drone (Raven)": ItemData(363 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 13, parent_item="Raven", origin={"nco"}),
"Spider Mines (Raven)": ItemData(364 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 14, parent_item="Raven", origin={"nco"}),
"Railgun Turret (Raven)": ItemData(365 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 15, parent_item="Raven", origin={"nco"}),
"Hunter-Seeker Weapon (Raven)": ItemData(366 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 16, parent_item="Raven", origin={"nco"}),
"Interference Matrix (Raven)": ItemData(367 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 17, parent_item="Raven", origin={"ext"}),
"Anti-Armor Missile (Raven)": ItemData(368 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 18, classification=ItemClassification.filler, parent_item="Raven", origin={"ext"}),
"Internal Tech Module (Raven)": ItemData(369 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 19, classification=ItemClassification.filler, parent_item="Raven", origin={"nco"}),
"EMP Shockwave (Science Vessel)": ItemData(370 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 20, parent_item="Science Vessel", origin={"bw"}),
"Defensive Matrix (Science Vessel)": ItemData(371 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 21, parent_item="Science Vessel", origin={"bw"}),
"Targeting Optics (Cyclone)": ItemData(372 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 22, parent_item="Cyclone", origin={"ext"}),
"Rapid Fire Launchers (Cyclone)": ItemData(373 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 23, parent_item="Cyclone", origin={"ext"}),
"Cloak (Liberator)": ItemData(374 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 24, classification=ItemClassification.filler, parent_item="Liberator", origin={"nco"}),
"Laser Targeting System (Liberator)": ItemData(375 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 25, classification=ItemClassification.filler, parent_item="Liberator", origin={"ext"}),
"Optimized Logistics (Liberator)": ItemData(376 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 26, classification=ItemClassification.filler, parent_item="Liberator", origin={"nco"}),
"Black Market Launchers (Widow Mine)": ItemData(377 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 27, classification=ItemClassification.filler, parent_item="Widow Mine", origin={"ext"}),
"Executioner Missiles (Widow Mine)": ItemData(378 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 28, parent_item="Widow Mine", origin={"ext"}),
# Just lazy to create a new group for one unit
"Enhanced Cluster Launchers (Valkyrie)": ItemData(379 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, parent_item="Valkyrie", origin={"ext"}),
"Shaped Hull (Valkyrie)": ItemData(380 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 20, classification=ItemClassification.filler, parent_item="Valkyrie", origin={"ext"}),
"Burst Lasers (Valkyrie)": ItemData(381 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 21, parent_item="Valkyrie", origin={"ext"}),
"Afterburners (Valkyrie)": ItemData(382 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 22, classification=ItemClassification.filler, parent_item="Valkyrie", origin={"ext"}),
"Bunker": ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, classification=ItemClassification.progression),
"Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression),
"Sensor Tower": ItemData(402 + SC2WOL_ITEM_ID_OFFSET, "Building", 2),
"War Pigs": ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0, classification=ItemClassification.progression),
"Devil Dogs": ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1, classification=ItemClassification.filler),
"Hammer Securities": ItemData(502 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 2),
"Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3, classification=ItemClassification.progression),
"Siege Breakers": ItemData(504 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 4),
"Hel's Angel": ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5, classification=ItemClassification.progression),
"Dusk Wings": ItemData(506 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 6),
"Jackson's Revenge": ItemData(507 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 7),
"Ultra-Capacitors": ItemData(600 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 0),
"Vanadium Plating": ItemData(601 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 1),
"Orbital Depots": ItemData(602 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 2),
"Micro-Filtering": ItemData(603 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 3),
"Automated Refinery": ItemData(604 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 4),
"Command Center Reactor": ItemData(605 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 5),
"Raven": ItemData(606 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 6),
"Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression),
"Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8),
"Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9),
"Shrike Turret (Bunker)": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"),
"Fortified Bunker (Bunker)": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"),
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression),
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression),
"Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler),
"Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression),
"Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16),
"Progressive Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 4, quantity=2),
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression),
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression),
"Zealot": ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, classification=ItemClassification.progression),
"Stalker": ItemData(701 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 1, classification=ItemClassification.progression),
"High Templar": ItemData(702 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 2, classification=ItemClassification.progression),
"Dark Templar": ItemData(703 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 3, classification=ItemClassification.progression),
"Immortal": ItemData(704 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 4, classification=ItemClassification.progression),
"Colossus": ItemData(705 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 5),
"Phoenix": ItemData(706 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 6, classification=ItemClassification.filler),
"Void Ray": ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, classification=ItemClassification.progression),
"Carrier": ItemData(708 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 8, classification=ItemClassification.progression),
# Filler items to fill remaining spots
"+15 Starting Minerals": ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, classification=ItemClassification.filler),
"+15 Starting Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, classification=ItemClassification.filler),
# This Filler item isn't placed by the generator yet unless plando'd
"+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler),
# This item is used to "remove" location from the game. Never placed unless plando'd
"Nothing": ItemData(803 + SC2WOL_ITEM_ID_OFFSET, "Nothing Group", 2, quantity=0, classification=ItemClassification.trap),
# "Keystone Piece": ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing)
}
def get_item_table(multiworld: MultiWorld, player: int):
return item_table
basic_units = {
'Marine',
'Marauder',
'Goliath',
'Hellion',
'Vulture'
}
advanced_basic_units = basic_units.union({
'Reaper',
'Diamondback',
'Viking'
})
def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]:
if get_option_value(multiworld, player, 'required_tactics') > 0:
return advanced_basic_units
else:
return basic_units
item_name_groups = {}
for item, data in get_full_item_list().items():
item_name_groups.setdefault(data.type, []).append(item)
if data.type in ("Armory 1", "Armory 2") and '(' in item:
short_name = item[:item.find(' (')]
item_name_groups[short_name] = [item]
item_name_groups["Missions"] = ["Beat " + mission_name for mission_name in vanilla_mission_req_table]
# Items that can be placed before resources if not already in
# General upgrades and Mercs
second_pass_placeable_items: typing.Tuple[str, ...] = (
# Buildings without upgrades
"Sensor Tower",
"Hive Mind Emulator",
"Psi Disrupter",
"Perdition Turret",
# General upgrades without any dependencies
"Advanced Construction (SCV)",
"Dual-Fusion Welders (SCV)",
"Fire-Suppression System (Building)",
"Orbital Command (Building)",
"Ultra-Capacitors",
"Vanadium Plating",
"Orbital Depots",
"Micro-Filtering",
"Automated Refinery",
"Command Center Reactor",
"Tech Reactor",
"Planetary Fortress",
"Cellular Reactor",
"Progressive Regenerative Bio-Steel", # Place only L1
# Mercenaries
"War Pigs",
"Devil Dogs",
"Hammer Securities",
"Spartan Company",
"Siege Breakers",
"Hel's Angel",
"Dusk Wings",
"Jackson's Revenge"
)
filler_items: typing.Tuple[str, ...] = (
'+15 Starting Minerals',
'+15 Starting Vespene'
)
# Defense rating table
# Commented defense ratings are handled in LogicMixin
defense_ratings = {
"Siege Tank": 5,
# "Maelstrom Rounds": 2,
"Planetary Fortress": 3,
# Bunker w/ Marine/Marauder: 3,
"Perdition Turret": 2,
"Missile Turret": 2,
"Vulture": 2,
"Liberator": 2,
"Widow Mine": 2
# "Concealment (Widow Mine)": 1
}
zerg_defense_ratings = {
"Perdition Turret": 2,
# Bunker w/ Firebat: 2,
"Hive Mind Emulator": 3,
"Psi Disruptor": 3
}
spider_mine_sources = {
"Vulture",
"Spider Mines (Reaper)",
"Spider Mines (Siege Tank)",
"Spider Mines (Raven)"
}
progressive_if_nco = {
"Progressive Stimpack (Marine)",
"Progressive Stimpack (Firebat)",
"Progressive Cross-Spectrum Dampeners (Banshee)",
"Progressive Regenerative Bio-Steel"
}
# 'number' values of upgrades for upgrade bundle items
upgrade_numbers = [
{0, 4, 8}, # Weapon
{2, 6, 10}, # Armor
{0, 2}, # Infantry
{4, 6}, # Vehicle
{8, 10}, # Starship
{0, 2, 4, 6, 8, 10} # All
]
# Names of upgrades to be included for different options
upgrade_included_names = [
{ # Individual Items
"Progressive Infantry Weapon",
"Progressive Infantry Armor",
"Progressive Vehicle Weapon",
"Progressive Vehicle Armor",
"Progressive Ship Weapon",
"Progressive Ship Armor"
},
{ # Bundle Weapon And Armor
"Progressive Weapon Upgrade",
"Progressive Armor Upgrade"
},
{ # Bundle Unit Class
"Progressive Infantry Upgrade",
"Progressive Vehicle Upgrade",
"Progressive Starship Upgrade"
},
{ # Bundle All
"Progressive Weapon/Armor Upgrade"
}
]
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
data.code}
# Map type to expected int
type_flaggroups: typing.Dict[str, int] = {
"Unit": 0,
"Upgrade": 1, # Weapon / Armor upgrades
"Armory 1": 2, # Unit upgrades
"Armory 2": 3, # Unit upgrades
"Building": 4,
"Mercenary": 5,
"Laboratory": 6,
"Protoss": 7,
"Minerals": 8,
"Vespene": 9,
"Supply": 10,
"Goal": 11,
"Armory 3": 12, # Unit upgrades
"Armory 4": 13, # Unit upgrades
"Progressive Upgrade": 14, # Unit upgrades that exist multiple times (Stimpack / Super Stimpack)
"Nothing Group": 15
}

View File

@ -1,516 +0,0 @@
from enum import IntEnum
from typing import List, Tuple, Optional, Callable, NamedTuple
from BaseClasses import MultiWorld
from .Options import get_option_value
from BaseClasses import Location
SC2WOL_LOC_ID_OFFSET = 1000
class SC2WoLLocation(Location):
game: str = "Starcraft2WoL"
class LocationType(IntEnum):
VICTORY = 0 # Winning a mission
MISSION_PROGRESS = 1 # All tasks done for progressing the mission normally towards victory. All cleaning of expansion bases falls here
BONUS = 2 # Bonus objective, getting a campaign or mission bonus in vanilla (credits, research, bonus units or resources)
CHALLENGE = 3 # Challenging objectives, often harder than just completing a mission
OPTIONAL_BOSS = 4 # Any boss that's not required to win the mission. All Brutalisks, Loki, etc.
class LocationData(NamedTuple):
region: str
name: str
code: Optional[int]
type: LocationType
rule: Callable = lambda state: True
def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
# Note: rules which are ended with or True are rules identified as needed later when restricted units is an option
logic_level = get_option_value(multiworld, player, 'required_tactics')
location_table: List[LocationData] = [
LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100, LocationType.VICTORY),
LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101, LocationType.BONUS),
LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102, LocationType.BONUS),
LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103, LocationType.BONUS),
LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104, LocationType.BONUS),
LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105, LocationType.BONUS),
LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106, LocationType.BONUS),
LocationData("Liberation Day", "Liberation Day: Special Delivery", SC2WOL_LOC_ID_OFFSET + 107, LocationType.MISSION_PROGRESS),
LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("The Outlaws", "The Outlaws: North Resource Pickups", SC2WOL_LOC_ID_OFFSET + 202, LocationType.BONUS),
LocationData("The Outlaws", "The Outlaws: Bunker", SC2WOL_LOC_ID_OFFSET + 203, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 2 and
(logic_level > 0 or state._sc2wol_has_anti_air(multiworld, player))),
LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301, LocationType.BONUS),
LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 2),
LocationData("Zero Hour", "Zero Hour: First Hatchery", SC2WOL_LOC_ID_OFFSET + 304, LocationType.CHALLENGE,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Zero Hour", "Zero Hour: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 305, LocationType.CHALLENGE,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Zero Hour", "Zero Hour: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 306, LocationType.CHALLENGE,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Zero Hour", "Zero Hour: Fourth Hatchery", SC2WOL_LOC_ID_OFFSET + 307, LocationType.CHALLENGE,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
LocationData("Evacuation", "Evacuation: North Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS),
LocationData("Evacuation", "Evacuation: West Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Evacuation", "Evacuation: East Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Evacuation", "Evacuation: Reach Hanson", SC2WOL_LOC_ID_OFFSET + 404, LocationType.MISSION_PROGRESS),
LocationData("Evacuation", "Evacuation: Secret Resource Stash", SC2WOL_LOC_ID_OFFSET + 405, LocationType.BONUS),
LocationData("Evacuation", "Evacuation: Flawless", SC2WOL_LOC_ID_OFFSET + 406, LocationType.CHALLENGE,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, LocationType.VICTORY,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 4 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: North Infested Command Center", SC2WOL_LOC_ID_OFFSET + 503, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: South Infested Command Center", SC2WOL_LOC_ID_OFFSET + 504, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: Northwest Bar", SC2WOL_LOC_ID_OFFSET + 505, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: North Bar", SC2WOL_LOC_ID_OFFSET + 506, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: South Bar", SC2WOL_LOC_ID_OFFSET + 507, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
LocationData("Safe Haven", "Safe Haven: First Terror Fleet", SC2WOL_LOC_ID_OFFSET + 604, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
LocationData("Safe Haven", "Safe Haven: Second Terror Fleet", SC2WOL_LOC_ID_OFFSET + 605, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
LocationData("Safe Haven", "Safe Haven: Third Terror Fleet", SC2WOL_LOC_ID_OFFSET + 606, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
LocationData("Haven's Fall", "Haven's Fall: Northeast Colony Base", SC2WOL_LOC_ID_OFFSET + 704, LocationType.CHALLENGE,
lambda state: state._sc2wol_can_respond_to_colony_infestations),
LocationData("Haven's Fall", "Haven's Fall: East Colony Base", SC2WOL_LOC_ID_OFFSET + 705, LocationType.CHALLENGE,
lambda state: state._sc2wol_can_respond_to_colony_infestations),
LocationData("Haven's Fall", "Haven's Fall: Middle Colony Base", SC2WOL_LOC_ID_OFFSET + 706, LocationType.CHALLENGE,
lambda state: state._sc2wol_can_respond_to_colony_infestations),
LocationData("Haven's Fall", "Haven's Fall: Southeast Colony Base", SC2WOL_LOC_ID_OFFSET + 707, LocationType.CHALLENGE,
lambda state: state._sc2wol_can_respond_to_colony_infestations),
LocationData("Haven's Fall", "Haven's Fall: Southwest Colony Base", SC2WOL_LOC_ID_OFFSET + 708, LocationType.CHALLENGE,
lambda state: state._sc2wol_can_respond_to_colony_infestations),
LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801, LocationType.BONUS),
LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802, LocationType.BONUS),
LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
LocationData("Smash and Grab", "Smash and Grab: First Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 805, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
LocationData("Smash and Grab", "Smash and Grab: Second Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 806, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, LocationType.VICTORY,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, False) >= 7),
LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
LocationData("The Dig", "The Dig: Moebius Base", SC2WOL_LOC_ID_OFFSET + 904, LocationType.MISSION_PROGRESS),
LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, LocationType.VICTORY,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
(state._sc2wol_has_air(multiworld, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(multiworld, player))),
LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core", SC2WOL_LOC_ID_OFFSET + 1001, LocationType.MISSION_PROGRESS),
LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, LocationType.MISSION_PROGRESS,
lambda state: (state._sc2wol_has_air(multiworld, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(multiworld, player))),
LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
(state._sc2wol_has_air(multiworld, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(multiworld, player))),
LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1009, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
(state._sc2wol_has_air(multiworld, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(multiworld, player))),
LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, LocationType.VICTORY,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101, LocationType.BONUS),
LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, LocationType.BONUS),
LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, LocationType.BONUS,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, LocationType.BONUS,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
LocationData("Supernova", "Supernova: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1105, LocationType.MISSION_PROGRESS),
LocationData("Supernova", "Supernova: Middle Base", SC2WOL_LOC_ID_OFFSET + 1106, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
LocationData("Supernova", "Supernova: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1107, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, LocationType.VICTORY,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201, LocationType.MISSION_PROGRESS),
LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, LocationType.BONUS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, LocationType.BONUS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: Mothership", SC2WOL_LOC_ID_OFFSET + 1206, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: Expansion Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1207, LocationType.MISSION_PROGRESS,
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: Middle Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1208, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: Southeast Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1209, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: Stargate Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1210, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: Northwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1211, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: West Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1212, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Maw of the Void", "Maw of the Void: Southwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1213, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, LocationType.VICTORY,
lambda state: logic_level > 0 or
state._sc2wol_has_anti_air(multiworld, player) and (
state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301, LocationType.BONUS),
LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, LocationType.OPTIONAL_BOSS,
lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
LocationData("Devil's Playground", "Devil's Playground: North Reapers", SC2WOL_LOC_ID_OFFSET + 1303, LocationType.BONUS),
LocationData("Devil's Playground", "Devil's Playground: Middle Reapers", SC2WOL_LOC_ID_OFFSET + 1304, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
LocationData("Devil's Playground", "Devil's Playground: Southwest Reapers", SC2WOL_LOC_ID_OFFSET + 1305, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
LocationData("Devil's Playground", "Devil's Playground: Southeast Reapers", SC2WOL_LOC_ID_OFFSET + 1306, LocationType.BONUS,
lambda state: logic_level > 0 or
state._sc2wol_has_anti_air(multiworld, player) and (
state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Devil's Playground", "Devil's Playground: East Reapers", SC2WOL_LOC_ID_OFFSET + 1307, LocationType.BONUS,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
(logic_level > 0 or
state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, LocationType.VICTORY,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401, LocationType.BONUS),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, LocationType.BONUS,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, LocationType.BONUS,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Middle Base", SC2WOL_LOC_ID_OFFSET + 1404, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Main Base", SC2WOL_LOC_ID_OFFSET + 1405, LocationType.CHALLENGE,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
and state._sc2wol_beats_protoss_deathball(multiworld, player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: No Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1406, LocationType.CHALLENGE,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
and state._sc2wol_has_competent_ground_to_air(multiworld, player)
and state._sc2wol_beats_protoss_deathball(multiworld, player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 1 Terrazine Node Sealed", SC2WOL_LOC_ID_OFFSET + 1407, LocationType.CHALLENGE,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
and state._sc2wol_has_competent_ground_to_air(multiworld, player)
and state._sc2wol_beats_protoss_deathball(multiworld, player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 2 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1408, LocationType.CHALLENGE,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
and state._sc2wol_beats_protoss_deathball(multiworld, player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 3 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1409, LocationType.CHALLENGE,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
and state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 4 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1410, LocationType.CHALLENGE,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 5 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1411, LocationType.CHALLENGE,
lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500, LocationType.VICTORY),
LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501, LocationType.BONUS),
LocationData("Breakout", "Breakout: Siege Tank Prison", SC2WOL_LOC_ID_OFFSET + 1502, LocationType.BONUS),
LocationData("Breakout", "Breakout: First Checkpoint", SC2WOL_LOC_ID_OFFSET + 1503, LocationType.MISSION_PROGRESS),
LocationData("Breakout", "Breakout: Second Checkpoint", SC2WOL_LOC_ID_OFFSET + 1504, LocationType.MISSION_PROGRESS),
LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600, LocationType.VICTORY),
LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601, LocationType.MISSION_PROGRESS),
LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602, LocationType.MISSION_PROGRESS),
LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603, LocationType.BONUS),
LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604, LocationType.BONUS),
LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605, LocationType.BONUS),
LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, LocationType.VICTORY,
lambda state: state._sc2wol_has_train_killers(multiworld, player) and
state._sc2wol_has_anti_air(multiworld, player)),
LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701, LocationType.BONUS),
LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702, LocationType.BONUS),
LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703, LocationType.BONUS),
LocationData("The Great Train Robbery", "The Great Train Robbery: Close Diamondback", SC2WOL_LOC_ID_OFFSET + 1704, LocationType.BONUS),
LocationData("The Great Train Robbery", "The Great Train Robbery: Northwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1705, LocationType.BONUS),
LocationData("The Great Train Robbery", "The Great Train Robbery: North Diamondback", SC2WOL_LOC_ID_OFFSET + 1706, LocationType.BONUS),
LocationData("The Great Train Robbery", "The Great Train Robbery: Northeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1707, LocationType.BONUS),
LocationData("The Great Train Robbery", "The Great Train Robbery: Southwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1708, LocationType.BONUS),
LocationData("The Great Train Robbery", "The Great Train Robbery: Southeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1709, LocationType.BONUS),
LocationData("The Great Train Robbery", "The Great Train Robbery: Kill Team", SC2WOL_LOC_ID_OFFSET + 1710, LocationType.CHALLENGE,
lambda state: (logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player)) and
state._sc2wol_has_train_killers(multiworld, player) and
state._sc2wol_has_anti_air(multiworld, player)),
LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 or state._sc2wol_has_anti_air)),
LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803, LocationType.BONUS),
LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Cutthroat", "Cutthroat: North Command Center", SC2WOL_LOC_ID_OFFSET + 1805, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Cutthroat", "Cutthroat: South Command Center", SC2WOL_LOC_ID_OFFSET + 1806, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Cutthroat", "Cutthroat: West Command Center", SC2WOL_LOC_ID_OFFSET + 1807, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, LocationType.VICTORY,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901, LocationType.MISSION_PROGRESS),
LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903, LocationType.BONUS),
LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
LocationData("Engine of Destruction", "Engine of Destruction: West Base", SC2WOL_LOC_ID_OFFSET + 1906, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
LocationData("Engine of Destruction", "Engine of Destruction: Northwest Base", SC2WOL_LOC_ID_OFFSET + 1907, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
LocationData("Engine of Destruction", "Engine of Destruction: Northeast Base", SC2WOL_LOC_ID_OFFSET + 1908, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
LocationData("Engine of Destruction", "Engine of Destruction: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1909, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, LocationType.VICTORY,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004, LocationType.BONUS),
LocationData("Media Blitz", "Media Blitz: All Barracks", SC2WOL_LOC_ID_OFFSET + 2005, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Media Blitz", "Media Blitz: All Factories", SC2WOL_LOC_ID_OFFSET + 2006, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Media Blitz", "Media Blitz: All Starports", SC2WOL_LOC_ID_OFFSET + 2007, LocationType.MISSION_PROGRESS,
lambda state: logic_level > 0 or state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Media Blitz", "Media Blitz: Odin Not Trashed", SC2WOL_LOC_ID_OFFSET + 2008, LocationType.CHALLENGE,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, LocationType.VICTORY,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101, LocationType.BONUS),
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, LocationType.BONUS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103,LocationType.BONUS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, LocationType.BONUS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: Fusion Reactor", SC2WOL_LOC_ID_OFFSET + 2106, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200, LocationType.VICTORY),
LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201, LocationType.BONUS),
LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202, LocationType.BONUS),
LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203, LocationType.BONUS),
LocationData("Whispers of Doom", "Whispers of Doom: First Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2204, LocationType.MISSION_PROGRESS),
LocationData("Whispers of Doom", "Whispers of Doom: Second Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2205, LocationType.MISSION_PROGRESS),
LocationData("Whispers of Doom", "Whispers of Doom: Third Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2206, LocationType.MISSION_PROGRESS),
LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, LocationType.VICTORY,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, LocationType.BONUS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Northeast Base", SC2WOL_LOC_ID_OFFSET + 2304, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Southwest Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Maar", SC2WOL_LOC_ID_OFFSET + 2306, LocationType.MISSION_PROGRESS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Northwest Preserver", SC2WOL_LOC_ID_OFFSET + 2307, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Southwest Preserver", SC2WOL_LOC_ID_OFFSET + 2308, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("A Sinister Turn", "A Sinister Turn: East Preserver", SC2WOL_LOC_ID_OFFSET + 2309, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, LocationType.VICTORY,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401, LocationType.BONUS),
LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
LocationData("Echoes of the Future", "Echoes of the Future: Base", SC2WOL_LOC_ID_OFFSET + 2403, LocationType.MISSION_PROGRESS),
LocationData("Echoes of the Future", "Echoes of the Future: Southwest Tendril", SC2WOL_LOC_ID_OFFSET + 2404, LocationType.MISSION_PROGRESS),
LocationData("Echoes of the Future", "Echoes of the Future: Southeast Tendril", SC2WOL_LOC_ID_OFFSET + 2405, LocationType.MISSION_PROGRESS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
LocationData("Echoes of the Future", "Echoes of the Future: Northeast Tendril", SC2WOL_LOC_ID_OFFSET + 2406, LocationType.MISSION_PROGRESS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
LocationData("Echoes of the Future", "Echoes of the Future: Northwest Tendril", SC2WOL_LOC_ID_OFFSET + 2407, LocationType.MISSION_PROGRESS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500, LocationType.VICTORY),
LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, LocationType.BONUS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, LocationType.CHALLENGE,
lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Urun", SC2WOL_LOC_ID_OFFSET + 2503, LocationType.MISSION_PROGRESS),
LocationData("In Utter Darkness", "In Utter Darkness: Mohandar", SC2WOL_LOC_ID_OFFSET + 2504, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Selendis", SC2WOL_LOC_ID_OFFSET + 2505, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Artanis", SC2WOL_LOC_ID_OFFSET + 2506, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, LocationType.VICTORY,
lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) > 6),
LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) > 6),
LocationData("Gates of Hell", "Gates of Hell: 2 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2602, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) > 6),
LocationData("Gates of Hell", "Gates of Hell: 4 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2603, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) > 6),
LocationData("Gates of Hell", "Gates of Hell: 6 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2604, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) > 6),
LocationData("Gates of Hell", "Gates of Hell: 8 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2605, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) > 6),
LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700, LocationType.VICTORY),
LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701, LocationType.MISSION_PROGRESS),
LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702, LocationType.MISSION_PROGRESS),
LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703, LocationType.MISSION_PROGRESS),
LocationData("Belly of the Beast", "Belly of the Beast: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 2704, LocationType.BONUS),
LocationData("Belly of the Beast", "Belly of the Beast: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 2705, LocationType.BONUS),
LocationData("Belly of the Beast", "Belly of the Beast: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 2706, LocationType.BONUS),
LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800, LocationType.VICTORY,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Shatter the Sky", "Shatter the Sky: East Hatchery", SC2WOL_LOC_ID_OFFSET + 2806, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Shatter the Sky", "Shatter the Sky: North Hatchery", SC2WOL_LOC_ID_OFFSET + 2807, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("Shatter the Sky", "Shatter the Sky: Mid Hatchery", SC2WOL_LOC_ID_OFFSET + 2808, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
LocationData("All-In", "All-In: Victory", None, LocationType.VICTORY,
lambda state: state._sc2wol_final_mission_requirements(multiworld, player))
]
beat_events = []
for i, location_data in enumerate(location_table):
# Removing all item-based logic on No Logic
if logic_level == 2:
location_data = location_data._replace(rule=Location.access_rule)
location_table[i] = location_data
# Generating Beat event locations
if location_data.name.endswith((": Victory", ": Defeat")):
beat_events.append(
location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None)
)
return tuple(location_table + beat_events)

View File

@ -1,148 +0,0 @@
from BaseClasses import MultiWorld
from worlds.AutoWorld import LogicMixin
from .Options import get_option_value
from .Items import get_basic_units, defense_ratings, zerg_defense_ratings
class SC2WoLLogic(LogicMixin):
def _sc2wol_has_common_unit(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any(get_basic_units(multiworld, player), player)
def _sc2wol_has_air(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any({'Viking', 'Wraith', 'Banshee', 'Battlecruiser'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 \
and self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(multiworld, player)
def _sc2wol_has_air_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
return self.has('Viking', player) \
or self.has_all({'Wraith', 'Advanced Laser Technology (Wraith)'}, player) \
or self.has_all({'Battlecruiser', 'ATX Laser Battery (Battlecruiser)'}, player) \
or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Wraith', 'Valkyrie', 'Battlecruiser'}, player)
def _sc2wol_has_competent_ground_to_air(self, multiworld: MultiWorld, player: int) -> bool:
return self.has('Goliath', player) \
or self.has('Marine', player) and self.has_any({'Medic', 'Medivac'}, player) \
or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Cyclone', player)
def _sc2wol_has_competent_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
return self._sc2wol_has_competent_ground_to_air(multiworld, player) \
or self._sc2wol_has_air_anti_air(multiworld, player)
def _sc2wol_welcome_to_the_jungle_requirement(self, multiworld: MultiWorld, player: int) -> bool:
return (
self._sc2wol_has_common_unit(multiworld, player)
and self._sc2wol_has_competent_ground_to_air(multiworld, player)
) or (
get_option_value(multiworld, player, 'required_tactics') > 0
and self.has_any({'Marine', 'Vulture'}, player)
and self._sc2wol_has_air_anti_air(multiworld, player)
)
def _sc2wol_has_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Marine', 'Wraith', 'Valkyrie', 'Cyclone'}, player) \
or self._sc2wol_has_competent_anti_air(multiworld, player) \
or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre', 'Widow Mine', 'Liberator'}, player)
def _sc2wol_defense_rating(self, multiworld: MultiWorld, player: int, zerg_enemy: bool, air_enemy: bool = True) -> bool:
defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player)))
if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player):
defense_score += 3
if self.has_all({'Siege Tank', 'Maelstrom Rounds (Siege Tank)'}, player):
defense_score += 2
if self.has_all({'Siege Tank', 'Graduating Range (Siege Tank)'}, player):
defense_score += 1
if self.has_all({'Widow Mine', 'Concealment (Widow Mine)'}, player):
defense_score += 1
if zerg_enemy:
defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player)))
if self.has('Firebat', player) and self.has('Bunker', player):
defense_score += 2
if not air_enemy and self.has('Missile Turret', player):
defense_score -= defense_ratings['Missile Turret']
# Advanced Tactics bumps defense rating requirements down by 2
if get_option_value(multiworld, player, 'required_tactics') > 0:
defense_score += 2
return defense_score
def _sc2wol_has_competent_comp(self, multiworld: MultiWorld, player: int) -> bool:
return \
(
(
self.has_any({'Marine', 'Marauder'}, player) and self.has_any({'Medivac', 'Medic'}, player)
or self.has_any({'Thor', 'Banshee', 'Siege Tank'}, player)
or self.has_all({'Liberator', 'Raid Artillery (Liberator)'}, player)
) and self._sc2wol_has_competent_anti_air(multiworld, player)
) \
or \
(
self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(multiworld, player)
)
def _sc2wol_has_train_killers(self, multiworld: MultiWorld, player: int) -> bool:
return (
self.has_any({'Siege Tank', 'Diamondback', 'Marauder', 'Cyclone'}, player)
or get_option_value(multiworld, player, 'required_tactics') > 0
and (
self.has_all({'Reaper', "G-4 Clusterbomb"}, player)
or self.has_all({'Spectre', 'Psionic Lash'}, player)
or self.has_any({'Vulture', 'Liberator'}, player)
)
)
def _sc2wol_able_to_rescue(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0
def _sc2wol_has_protoss_common_units(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \
or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('High Templar', player)
def _sc2wol_has_protoss_medium_units(self, multiworld: MultiWorld, player: int) -> bool:
return self._sc2wol_has_protoss_common_units(multiworld, player) and \
self.has_any({'Stalker', 'Void Ray', 'Carrier'}, player) \
or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Dark Templar', player)
def _sc2wol_beats_protoss_deathball(self, multiworld: MultiWorld, player: int) -> bool:
return (self.has_any({'Banshee', 'Battlecruiser'}, player) or
self.has_all({'Liberator', 'Raid Artillery (Liberator)'}, player)) \
and self._sc2wol_has_competent_anti_air(multiworld, player) or \
self._sc2wol_has_competent_comp(multiworld, player) and self._sc2wol_has_air_anti_air(multiworld, player)
def _sc2wol_has_mm_upgrade(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player)
def _sc2wol_survives_rip_field(self, multiworld: MultiWorld, player: int) -> bool:
return self.has("Battlecruiser", player) or \
self._sc2wol_has_air(multiworld, player) and \
self._sc2wol_has_competent_anti_air(multiworld, player) and \
self.has("Science Vessel", player)
def _sc2wol_has_nukes(self, multiworld: MultiWorld, player: int) -> bool:
return get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
def _sc2wol_can_respond_to_colony_infestations(self, multiworld: MultiWorld, player: int) -> bool:
return self._sc2wol_has_common_unit(multiworld, player) \
and self._sc2wol_has_competent_anti_air(multiworld, player) \
and \
(
self._sc2wol_has_air_anti_air(multiworld, player) or
self.has_any({'Battlecruiser', 'Valkyrie'}), player
) \
and \
self._sc2wol_defense_rating(multiworld, player, True) >= 3
def _sc2wol_final_mission_requirements(self, multiworld: MultiWorld, player: int):
beats_kerrigan = self.has_any({'Marine', 'Banshee', 'Ghost'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0
if get_option_value(multiworld, player, 'all_in_map') == 0:
# Ground
defense_rating = self._sc2wol_defense_rating(multiworld, player, True, False)
if self.has_any({'Battlecruiser', 'Banshee'}, player):
defense_rating += 3
return defense_rating >= 12 and beats_kerrigan
else:
# Air
defense_rating = self._sc2wol_defense_rating(multiworld, player, True, True)
return defense_rating >= 8 and beats_kerrigan \
and self.has_any({'Viking', 'Battlecruiser', 'Valkyrie'}, player) \
and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player)
def _sc2wol_cleared_missions(self, multiworld: MultiWorld, player: int, mission_count: int) -> bool:
return self.has_group("Missions", player, mission_count)

View File

@ -1,230 +0,0 @@
from typing import NamedTuple, Dict, List
from enum import IntEnum
no_build_regions_list = ["Liberation Day", "Breakout", "Ghost of a Chance", "Piercing the Shroud", "Whispers of Doom",
"Belly of the Beast"]
easy_regions_list = ["The Outlaws", "Zero Hour", "Evacuation", "Outbreak", "Smash and Grab", "Devil's Playground"]
medium_regions_list = ["Safe Haven", "Haven's Fall", "The Dig", "The Moebius Factor", "Supernova",
"Welcome to the Jungle", "The Great Train Robbery", "Cutthroat", "Media Blitz",
"A Sinister Turn", "Echoes of the Future"]
hard_regions_list = ["Maw of the Void", "Engine of Destruction", "In Utter Darkness", "Gates of Hell",
"Shatter the Sky"]
class MissionPools(IntEnum):
STARTER = 0
EASY = 1
MEDIUM = 2
HARD = 3
FINAL = 4
class MissionInfo(NamedTuple):
id: int
required_world: List[int]
category: str
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
class FillMission(NamedTuple):
type: int
connect_to: List[int] # -1 connects to Menu
category: str
number: int = 0 # number of worlds need beaten
completion_critical: bool = False # missions needed to beat game
or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
removal_priority: int = 0 # how many missions missing from the pool required to remove this mission
vanilla_shuffle_order = [
FillMission(MissionPools.STARTER, [-1], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [0], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [1], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [2], "Colonist"),
FillMission(MissionPools.MEDIUM, [3], "Colonist"),
FillMission(MissionPools.HARD, [4], "Colonist", number=7),
FillMission(MissionPools.HARD, [4], "Colonist", number=7, removal_priority=1),
FillMission(MissionPools.EASY, [2], "Artifact", completion_critical=True),
FillMission(MissionPools.MEDIUM, [7], "Artifact", number=8, completion_critical=True),
FillMission(MissionPools.HARD, [8], "Artifact", number=11, completion_critical=True),
FillMission(MissionPools.HARD, [9], "Artifact", number=14, completion_critical=True, removal_priority=11),
FillMission(MissionPools.HARD, [10], "Artifact", completion_critical=True, removal_priority=10),
FillMission(MissionPools.MEDIUM, [2], "Covert", number=4),
FillMission(MissionPools.MEDIUM, [12], "Covert"),
FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=3),
FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=2),
FillMission(MissionPools.MEDIUM, [2], "Rebellion", number=6),
FillMission(MissionPools.HARD, [16], "Rebellion"),
FillMission(MissionPools.HARD, [17], "Rebellion"),
FillMission(MissionPools.HARD, [18], "Rebellion", removal_priority=12),
FillMission(MissionPools.HARD, [19], "Rebellion", removal_priority=5),
FillMission(MissionPools.MEDIUM, [8], "Prophecy", removal_priority=9),
FillMission(MissionPools.HARD, [21], "Prophecy", removal_priority=8),
FillMission(MissionPools.HARD, [22], "Prophecy", removal_priority=7),
FillMission(MissionPools.HARD, [23], "Prophecy", removal_priority=6),
FillMission(MissionPools.HARD, [11], "Char", completion_critical=True),
FillMission(MissionPools.HARD, [25], "Char", completion_critical=True, removal_priority=4),
FillMission(MissionPools.HARD, [25], "Char", completion_critical=True),
FillMission(MissionPools.FINAL, [26, 27], "Char", completion_critical=True, or_requirements=True)
]
mini_campaign_order = [
FillMission(MissionPools.STARTER, [-1], "Mar Sara", completion_critical=True),
FillMission(MissionPools.EASY, [0], "Colonist"),
FillMission(MissionPools.MEDIUM, [1], "Colonist"),
FillMission(MissionPools.EASY, [0], "Artifact", completion_critical=True),
FillMission(MissionPools.MEDIUM, [3], "Artifact", number=4, completion_critical=True),
FillMission(MissionPools.HARD, [4], "Artifact", number=8, completion_critical=True),
FillMission(MissionPools.MEDIUM, [0], "Covert", number=2),
FillMission(MissionPools.HARD, [6], "Covert"),
FillMission(MissionPools.MEDIUM, [0], "Rebellion", number=3),
FillMission(MissionPools.HARD, [8], "Rebellion"),
FillMission(MissionPools.MEDIUM, [4], "Prophecy"),
FillMission(MissionPools.HARD, [10], "Prophecy"),
FillMission(MissionPools.HARD, [5], "Char", completion_critical=True),
FillMission(MissionPools.HARD, [5], "Char", completion_critical=True),
FillMission(MissionPools.FINAL, [12, 13], "Char", completion_critical=True, or_requirements=True)
]
gauntlet_order = [
FillMission(MissionPools.STARTER, [-1], "I", completion_critical=True),
FillMission(MissionPools.EASY, [0], "II", completion_critical=True),
FillMission(MissionPools.EASY, [1], "III", completion_critical=True),
FillMission(MissionPools.MEDIUM, [2], "IV", completion_critical=True),
FillMission(MissionPools.MEDIUM, [3], "V", completion_critical=True),
FillMission(MissionPools.HARD, [4], "VI", completion_critical=True),
FillMission(MissionPools.FINAL, [5], "Final", completion_critical=True)
]
mini_gauntlet_order = [
FillMission(MissionPools.STARTER, [-1], "I", completion_critical=True),
FillMission(MissionPools.EASY, [0], "II", completion_critical=True),
FillMission(MissionPools.MEDIUM, [1], "III", completion_critical=True),
FillMission(MissionPools.FINAL, [2], "Final", completion_critical=True)
]
grid_order = [
FillMission(MissionPools.STARTER, [-1], "_1"),
FillMission(MissionPools.EASY, [0], "_1"),
FillMission(MissionPools.MEDIUM, [1, 6, 3], "_1", or_requirements=True),
FillMission(MissionPools.HARD, [2, 7], "_1", or_requirements=True),
FillMission(MissionPools.EASY, [0], "_2"),
FillMission(MissionPools.MEDIUM, [1, 4], "_2", or_requirements=True),
FillMission(MissionPools.HARD, [2, 5, 10, 7], "_2", or_requirements=True),
FillMission(MissionPools.HARD, [3, 6, 11], "_2", or_requirements=True),
FillMission(MissionPools.MEDIUM, [4, 9, 12], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [5, 8, 10, 13], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [6, 9, 11, 14], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [7, 10], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [8, 13], "_4", or_requirements=True),
FillMission(MissionPools.HARD, [9, 12, 14], "_4", or_requirements=True),
FillMission(MissionPools.HARD, [10, 13], "_4", or_requirements=True),
FillMission(MissionPools.FINAL, [11, 14], "_4", or_requirements=True)
]
mini_grid_order = [
FillMission(MissionPools.STARTER, [-1], "_1"),
FillMission(MissionPools.EASY, [0], "_1"),
FillMission(MissionPools.MEDIUM, [1, 5], "_1", or_requirements=True),
FillMission(MissionPools.EASY, [0], "_2"),
FillMission(MissionPools.MEDIUM, [1, 3], "_2", or_requirements=True),
FillMission(MissionPools.HARD, [2, 4], "_2", or_requirements=True),
FillMission(MissionPools.MEDIUM, [3, 7], "_3", or_requirements=True),
FillMission(MissionPools.HARD, [4, 6], "_3", or_requirements=True),
FillMission(MissionPools.FINAL, [5, 7], "_3", or_requirements=True)
]
tiny_grid_order = [
FillMission(MissionPools.STARTER, [-1], "_1"),
FillMission(MissionPools.MEDIUM, [0], "_1"),
FillMission(MissionPools.EASY, [0], "_2"),
FillMission(MissionPools.FINAL, [1, 2], "_2", or_requirements=True),
]
blitz_order = [
FillMission(MissionPools.STARTER, [-1], "I"),
FillMission(MissionPools.EASY, [-1], "I"),
FillMission(MissionPools.MEDIUM, [0, 1], "II", number=1, or_requirements=True),
FillMission(MissionPools.MEDIUM, [0, 1], "II", number=1, or_requirements=True),
FillMission(MissionPools.MEDIUM, [0, 1], "III", number=2, or_requirements=True),
FillMission(MissionPools.MEDIUM, [0, 1], "III", number=2, or_requirements=True),
FillMission(MissionPools.HARD, [0, 1], "IV", number=3, or_requirements=True),
FillMission(MissionPools.HARD, [0, 1], "IV", number=3, or_requirements=True),
FillMission(MissionPools.HARD, [0, 1], "V", number=4, or_requirements=True),
FillMission(MissionPools.HARD, [0, 1], "V", number=4, or_requirements=True),
FillMission(MissionPools.HARD, [0, 1], "Final", number=5, or_requirements=True),
FillMission(MissionPools.FINAL, [0, 1], "Final", number=5, or_requirements=True)
]
mission_orders = [
vanilla_shuffle_order,
vanilla_shuffle_order,
mini_campaign_order,
grid_order,
mini_grid_order,
blitz_order,
gauntlet_order,
mini_gauntlet_order,
tiny_grid_order
]
vanilla_mission_req_table = {
"Liberation Day": MissionInfo(1, [], "Mar Sara", completion_critical=True),
"The Outlaws": MissionInfo(2, [1], "Mar Sara", completion_critical=True),
"Zero Hour": MissionInfo(3, [2], "Mar Sara", completion_critical=True),
"Evacuation": MissionInfo(4, [3], "Colonist"),
"Outbreak": MissionInfo(5, [4], "Colonist"),
"Safe Haven": MissionInfo(6, [5], "Colonist", number=7),
"Haven's Fall": MissionInfo(7, [5], "Colonist", number=7),
"Smash and Grab": MissionInfo(8, [3], "Artifact", completion_critical=True),
"The Dig": MissionInfo(9, [8], "Artifact", number=8, completion_critical=True),
"The Moebius Factor": MissionInfo(10, [9], "Artifact", number=11, completion_critical=True),
"Supernova": MissionInfo(11, [10], "Artifact", number=14, completion_critical=True),
"Maw of the Void": MissionInfo(12, [11], "Artifact", completion_critical=True),
"Devil's Playground": MissionInfo(13, [3], "Covert", number=4),
"Welcome to the Jungle": MissionInfo(14, [13], "Covert"),
"Breakout": MissionInfo(15, [14], "Covert", number=8),
"Ghost of a Chance": MissionInfo(16, [14], "Covert", number=8),
"The Great Train Robbery": MissionInfo(17, [3], "Rebellion", number=6),
"Cutthroat": MissionInfo(18, [17], "Rebellion"),
"Engine of Destruction": MissionInfo(19, [18], "Rebellion"),
"Media Blitz": MissionInfo(20, [19], "Rebellion"),
"Piercing the Shroud": MissionInfo(21, [20], "Rebellion"),
"Whispers of Doom": MissionInfo(22, [9], "Prophecy"),
"A Sinister Turn": MissionInfo(23, [22], "Prophecy"),
"Echoes of the Future": MissionInfo(24, [23], "Prophecy"),
"In Utter Darkness": MissionInfo(25, [24], "Prophecy"),
"Gates of Hell": MissionInfo(26, [12], "Char", completion_critical=True),
"Belly of the Beast": MissionInfo(27, [26], "Char", completion_critical=True),
"Shatter the Sky": MissionInfo(28, [26], "Char", completion_critical=True),
"All-In": MissionInfo(29, [27, 28], "Char", completion_critical=True, or_requirements=True)
}
lookup_id_to_mission: Dict[int, str] = {
data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id}
starting_mission_locations = {
"Liberation Day": "Liberation Day: Victory",
"Breakout": "Breakout: Victory",
"Ghost of a Chance": "Ghost of a Chance: Victory",
"Piercing the Shroud": "Piercing the Shroud: Victory",
"Whispers of Doom": "Whispers of Doom: Victory",
"Belly of the Beast": "Belly of the Beast: Victory",
"Zero Hour": "Zero Hour: First Group Rescued",
"Evacuation": "Evacuation: Reach Hanson",
"Devil's Playground": "Devil's Playground: Tosh's Miners",
"Smash and Grab": "Smash and Grab: First Relic",
"The Great Train Robbery": "The Great Train Robbery: North Defiler"
}
alt_final_mission_locations = {
"Maw of the Void": "Maw of the Void: Victory",
"Engine of Destruction": "Engine of Destruction: Victory",
"Supernova": "Supernova: Victory",
"Gates of Hell": "Gates of Hell: Victory",
"Shatter the Sky": "Shatter the Sky: Victory"
}

View File

@ -1,362 +0,0 @@
from typing import Dict, FrozenSet, Union
from BaseClasses import MultiWorld
from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range
from .MissionTables import vanilla_mission_req_table
ORDER_VANILLA = 0
ORDER_VANILLA_SHUFFLED = 1
class GameDifficulty(Choice):
"""
The difficulty of the campaign, affects enemy AI, starting units, and game speed.
For those unfamiliar with the Archipelago randomizer, the recommended settings are one difficulty level
lower than the vanilla game
"""
display_name = "Game Difficulty"
option_casual = 0
option_normal = 1
option_hard = 2
option_brutal = 3
default = 1
class GameSpeed(Choice):
"""Optional setting to override difficulty-based game speed."""
display_name = "Game Speed"
option_default = 0
option_slower = 1
option_slow = 2
option_normal = 3
option_fast = 4
option_faster = 5
default = option_default
class FinalMap(Choice):
"""
Determines if the final map and goal of the campaign.
All in: You need to beat All-in map
Random Hard: A random hard mission is selected as a goal.
Beat this mission in order to complete the game.
All-in map won't be in the campaign
Vanilla mission order always ends with All in mission!
Warning: Using All-in with a short mission order (7 or fewer missions) is not recommended,
as there might not be enough locations to place all the required items,
any excess required items will be placed into the player's starting inventory!
This option is short-lived. It may be changed in the future
"""
display_name = "Final Map"
option_all_in = 0
option_random_hard = 1
class AllInMap(Choice):
"""Determines what version of All-In (final map) that will be generated for the campaign."""
display_name = "All In Map"
option_ground = 0
option_air = 1
class MissionOrder(Choice):
"""
Determines the order the missions are played in. The last three mission orders end in a random mission.
Vanilla (29): Keeps the standard mission order and branching from the WoL Campaign.
Vanilla Shuffled (29): Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.
Mini Campaign (15): Shorter version of the campaign with randomized missions and optional branches.
Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards bottom-right mission to win.
Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win.
Blitz (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win.
Gauntlet (7): Linear series of 7 random missions to complete the campaign.
Mini Gauntlet (4): Linear series of 4 random missions to complete the campaign.
Tiny Grid (4): A 2x2 version of Grid. Complete the bottom-right mission to win.
"""
display_name = "Mission Order"
option_vanilla = 0
option_vanilla_shuffled = 1
option_mini_campaign = 2
option_grid = 3
option_mini_grid = 4
option_blitz = 5
option_gauntlet = 6
option_mini_gauntlet = 7
option_tiny_grid = 8
class PlayerColor(Choice):
"""Determines in-game team color."""
display_name = "Player Color"
option_white = 0
option_red = 1
option_blue = 2
option_teal = 3
option_purple = 4
option_yellow = 5
option_orange = 6
option_green = 7
option_light_pink = 8
option_violet = 9
option_light_grey = 10
option_dark_green = 11
option_brown = 12
option_light_green = 13
option_dark_grey = 14
option_pink = 15
option_rainbow = 16
option_default = 17
default = option_default
class ShuffleProtoss(DefaultOnToggle):
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled.
If turned off, the 3 protoss missions will not appear and Protoss units are removed from the pool."""
display_name = "Shuffle Protoss Missions"
class ShuffleNoBuild(DefaultOnToggle):
"""Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
If turned off, the 5 no-build missions will not appear."""
display_name = "Shuffle No-Build Missions"
class EarlyUnit(DefaultOnToggle):
"""
Guarantees that the first mission will contain a unit.
Each mission available to be the first mission has a pre-defined location where the unit should spawn.
This location gets overriden over any exclusion. It's guaranteed to be reachable with an empty inventory.
"""
display_name = "Early Unit"
class RequiredTactics(Choice):
"""Determines the maximum tactical difficulty of the seed (separate from mission difficulty). Higher settings
increase randomness.
Standard: All missions can be completed with good micro and macro.
Advanced: Completing missions may require relying on starting units and micro-heavy units.
No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!"""
display_name = "Required Tactics"
option_standard = 0
option_advanced = 1
option_no_logic = 2
class UnitsAlwaysHaveUpgrades(DefaultOnToggle):
"""
If turned on, all upgrades will be present for each unit and structure in the seed.
This usually results in fewer units.
See also: Max Number of Upgrades
"""
display_name = "Units Always Have Upgrades"
class GenericUpgradeMissions(Range):
"""Determines the percentage of missions in the mission order that must be completed before
level 1 of all weapon and armor upgrades is unlocked. Level 2 upgrades require double the amount of missions,
and level 3 requires triple the amount. The required amounts are always rounded down.
If set to 0, upgrades are instead added to the item pool and must be found to be used."""
display_name = "Generic Upgrade Missions"
range_start = 0
range_end = 100
default = 0
class GenericUpgradeResearch(Choice):
"""Determines how weapon and armor upgrades affect missions once unlocked.
Vanilla: Upgrades must be researched as normal.
Auto In No-Build: In No-Build missions, upgrades are automatically researched.
In all other missions, upgrades must be researched as normal.
Auto In Build: In No-Build missions, upgrades are unavailable as normal.
In all other missions, upgrades are automatically researched.
Always Auto: Upgrades are automatically researched in all missions."""
display_name = "Generic Upgrade Research"
option_vanilla = 0
option_auto_in_no_build = 1
option_auto_in_build = 2
option_always_auto = 3
class GenericUpgradeItems(Choice):
"""Determines how weapon and armor upgrades are split into items. All options produce 3 levels of each item.
Does nothing if upgrades are unlocked by completed mission counts.
Individual Items: All weapon and armor upgrades are each an item,
resulting in 18 total upgrade items.
Bundle Weapon And Armor: All types of weapon upgrades are one item,
and all types of armor upgrades are one item,
resulting in 6 total items.
Bundle Unit Class: Weapon and armor upgrades are merged,
but Infantry, Vehicle, and Starship upgrades are bundled separately,
resulting in 9 total items.
Bundle All: All weapon and armor upgrades are one item,
resulting in 3 total items."""
display_name = "Generic Upgrade Items"
option_individual_items = 0
option_bundle_weapon_and_armor = 1
option_bundle_unit_class = 2
option_bundle_all = 3
class NovaCovertOpsItems(Toggle):
"""If turned on, the equipment upgrades from Nova Covert Ops may be present in the world."""
display_name = "Nova Covert Ops Items"
default = Toggle.option_true
class BroodWarItems(Toggle):
"""If turned on, returning items from StarCraft: Brood War may appear in the world."""
display_name = "Brood War Items"
default = Toggle.option_true
class ExtendedItems(Toggle):
"""If turned on, original items that did not appear in Campaign mode may appear in the world."""
display_name = "Extended Items"
default = Toggle.option_true
class MaxNumberOfUpgrades(Range):
"""
Set a maximum to the number of upgrades a unit/structure can have. -1 is used to define unlimited.
Note that most unit have 4 or 6 upgrades.
If used with Units Always Have Upgrades, each unit has this given amount of upgrades (if there enough upgrades exist)
See also: Units Always Have Upgrades
"""
display_name = "Maximum number of upgrades per unit/structure"
range_start = -1
# Do not know the maximum, but it is less than 123!
range_end = 123
default = -1
class LockedItems(ItemSet):
"""Guarantees that these items will be unlockable"""
display_name = "Locked Items"
class ExcludedItems(ItemSet):
"""Guarantees that these items will not be unlockable"""
display_name = "Excluded Items"
class ExcludedMissions(OptionSet):
"""Guarantees that these missions will not appear in the campaign
Doesn't apply to vanilla mission order.
It may be impossible to build a valid campaign if too many missions are excluded."""
display_name = "Excluded Missions"
valid_keys = {mission_name for mission_name in vanilla_mission_req_table.keys() if mission_name != 'All-In'}
class LocationInclusion(Choice):
option_enabled = 0
option_trash = 1
option_nothing = 2
class MissionProgressLocations(LocationInclusion):
"""
Enables or disables item rewards for progressing (not finishing) a mission.
Progressing a mission is usually a task of completing or progressing into a main objective.
Clearing an expansion base also counts here.
Enabled: All locations fitting into this do their normal rewards
Trash: Forces a trash item in
Nothing: No rewards for this type of tasks, effectively disabling such locations
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Mission Progress Locations"
class BonusLocations(LocationInclusion):
"""
Enables or disables item rewards for completing bonus tasks.
Bonus tasks are those giving you a campaign-wide or mission-wide bonus in vanilla game:
Research, credits, bonus units or resources, etc.
Enabled: All locations fitting into this do their normal rewards
Trash: Forces a trash item in
Nothing: No rewards for this type of tasks, effectively disabling such locations
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Bonus Locations"
class ChallengeLocations(LocationInclusion):
"""
Enables or disables item rewards for completing challenge tasks.
Challenges are tasks that have usually higher requirements to be completed
than to complete the mission they're in successfully.
You might be required to visit the same mission later when getting stronger in order to finish these tasks.
Enabled: All locations fitting into this do their normal rewards
Trash: Forces a trash item in
Nothing: No rewards for this type of tasks, effectively disabling such locations
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Challenge Locations"
class OptionalBossLocations(LocationInclusion):
"""
Enables or disables item rewards for defeating optional bosses.
An optional boss is any boss that's not required to kill in order to finish the mission successfully.
All Brutalisks, Loki, etc. belongs here.
Enabled: All locations fitting into this do their normal rewards
Trash: Forces a trash item in
Nothing: No rewards for this type of tasks, effectively disabling such locations
Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
"""
display_name = "Optional Boss Locations"
# noinspection PyTypeChecker
sc2wol_options: Dict[str, Option] = {
"game_difficulty": GameDifficulty,
"game_speed": GameSpeed,
"all_in_map": AllInMap,
"final_map": FinalMap,
"mission_order": MissionOrder,
"player_color": PlayerColor,
"shuffle_protoss": ShuffleProtoss,
"shuffle_no_build": ShuffleNoBuild,
"early_unit": EarlyUnit,
"required_tactics": RequiredTactics,
"units_always_have_upgrades": UnitsAlwaysHaveUpgrades,
"max_number_of_upgrades": MaxNumberOfUpgrades,
"generic_upgrade_missions": GenericUpgradeMissions,
"generic_upgrade_research": GenericUpgradeResearch,
"generic_upgrade_items": GenericUpgradeItems,
"locked_items": LockedItems,
"excluded_items": ExcludedItems,
"excluded_missions": ExcludedMissions,
"nco_items": NovaCovertOpsItems,
"bw_items": BroodWarItems,
"ext_items": ExtendedItems,
"mission_progress_locations": MissionProgressLocations,
"bonus_locations": BonusLocations,
"challenge_locations": ChallengeLocations,
"optional_boss_locations": OptionalBossLocations
}
def get_option_value(multiworld: MultiWorld, player: int, name: str) -> Union[int, FrozenSet]:
if multiworld is None:
return sc2wol_options[name].default
player_option = getattr(multiworld, name)[player]
return player_option.value

View File

@ -1,367 +0,0 @@
from typing import Callable, Dict, List, Set
from BaseClasses import MultiWorld, ItemClassification, Item, Location
from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items, \
progressive_if_nco
from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\
mission_orders, MissionInfo, alt_final_mission_locations, MissionPools
from .Options import get_option_value, MissionOrder, FinalMap, MissionProgressLocations, LocationInclusion
from .LogicMixin import SC2WoLLogic
# Items with associated upgrades
UPGRADABLE_ITEMS = [
"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre",
"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine", "Cyclone",
"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Raven", "Science Vessel", "Liberator", "Valkyrie",
"Bunker", "Missile Turret"
]
BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"}
FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine", "Cyclone"}
STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven", "Liberator", "Valkyrie"}
PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"}
def filter_missions(multiworld: MultiWorld, player: int) -> Dict[int, List[str]]:
"""
Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets
"""
mission_order_type = get_option_value(multiworld, player, "mission_order")
shuffle_no_build = get_option_value(multiworld, player, "shuffle_no_build")
shuffle_protoss = get_option_value(multiworld, player, "shuffle_protoss")
excluded_missions = get_option_value(multiworld, player, "excluded_missions")
final_map = get_option_value(multiworld, player, "final_map")
mission_pools = {
MissionPools.STARTER: no_build_regions_list[:],
MissionPools.EASY: easy_regions_list[:],
MissionPools.MEDIUM: medium_regions_list[:],
MissionPools.HARD: hard_regions_list[:],
MissionPools.FINAL: []
}
if mission_order_type == MissionOrder.option_vanilla:
# Vanilla uses the entire mission pool
mission_pools[MissionPools.FINAL] = ['All-In']
return mission_pools
# Omitting No-Build missions if not shuffling no-build
if not shuffle_no_build:
excluded_missions = excluded_missions.union(no_build_regions_list)
# Omitting Protoss missions if not shuffling protoss
if not shuffle_protoss:
excluded_missions = excluded_missions.union(PROTOSS_REGIONS)
# Replacing All-In with alternate ending depending on option
if final_map == FinalMap.option_random_hard:
final_mission = multiworld.random.choice([mission for mission in alt_final_mission_locations.keys() if mission not in excluded_missions])
excluded_missions.add(final_mission)
else:
final_mission = 'All-In'
# Excluding missions
for difficulty, mission_pool in mission_pools.items():
mission_pools[difficulty] = [mission for mission in mission_pool if mission not in excluded_missions]
mission_pools[MissionPools.FINAL].append(final_mission)
# Mission pool changes on Build-Only
if not get_option_value(multiworld, player, 'shuffle_no_build'):
def move_mission(mission_name, current_pool, new_pool):
if mission_name in mission_pools[current_pool]:
mission_pools[current_pool].remove(mission_name)
mission_pools[new_pool].append(mission_name)
# Replacing No Build missions with Easy missions
move_mission("Zero Hour", MissionPools.EASY, MissionPools.STARTER)
move_mission("Evacuation", MissionPools.EASY, MissionPools.STARTER)
move_mission("Devil's Playground", MissionPools.EASY, MissionPools.STARTER)
# Pushing Outbreak to Normal, as it cannot be placed as the second mission on Build-Only
move_mission("Outbreak", MissionPools.EASY, MissionPools.MEDIUM)
# Pushing extra Normal missions to Easy
move_mission("The Great Train Robbery", MissionPools.MEDIUM, MissionPools.EASY)
move_mission("Echoes of the Future", MissionPools.MEDIUM, MissionPools.EASY)
move_mission("Cutthroat", MissionPools.MEDIUM, MissionPools.EASY)
# Additional changes on Advanced Tactics
if get_option_value(multiworld, player, "required_tactics") > 0:
move_mission("The Great Train Robbery", MissionPools.EASY, MissionPools.STARTER)
move_mission("Smash and Grab", MissionPools.EASY, MissionPools.STARTER)
move_mission("Moebius Factor", MissionPools.MEDIUM, MissionPools.EASY)
move_mission("Welcome to the Jungle", MissionPools.MEDIUM, MissionPools.EASY)
move_mission("Engine of Destruction", MissionPools.HARD, MissionPools.MEDIUM)
return mission_pools
def get_item_upgrades(inventory: List[Item], parent_item: Item or str):
item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
return [
inv_item for inv_item in inventory
if get_full_item_list()[inv_item.name].parent_item == item_name
]
def get_item_quantity(item: Item, multiworld: MultiWorld, player: int):
if (not get_option_value(multiworld, player, "nco_items")) \
and item.name in progressive_if_nco:
return 1
return get_full_item_list()[item.name].quantity
def copy_item(item: Item):
return Item(item.name, item.classification, item.code, item.player)
class ValidInventory:
def has(self, item: str, player: int):
return item in self.logical_inventory
def has_any(self, items: Set[str], player: int):
return any(item in self.logical_inventory for item in items)
def has_all(self, items: Set[str], player: int):
return all(item in self.logical_inventory for item in items)
def has_units_per_structure(self) -> bool:
return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure
def generate_reduced_inventory(self, inventory_size: int, mission_requirements: List[Callable]) -> List[Item]:
"""Attempts to generate a reduced inventory that can fulfill the mission requirements."""
inventory = list(self.item_pool)
locked_items = list(self.locked_items)
self.logical_inventory = {
item.name for item in inventory + locked_items + self.existing_items
if item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing)
}
requirements = mission_requirements
cascade_keys = self.cascade_removal_map.keys()
units_always_have_upgrades = get_option_value(self.multiworld, self.player, "units_always_have_upgrades")
def attempt_removal(item: Item) -> bool:
# If item can be removed and has associated items, remove them as well
inventory.remove(item)
# Only run logic checks when removing logic items
if item.name in self.logical_inventory:
self.logical_inventory.remove(item.name)
if not all(requirement(self) for requirement in requirements):
# If item cannot be removed, lock or revert
self.logical_inventory.add(item.name)
for _ in range(get_item_quantity(item, self.multiworld, self.player)):
locked_items.append(copy_item(item))
return False
return True
# Limit the maximum number of upgrades
maxUpgrad = get_option_value(self.multiworld, self.player,
"max_number_of_upgrades")
if maxUpgrad != -1:
unit_avail_upgrades = {}
# Needed to take into account locked/existing items
unit_nb_upgrades = {}
for item in inventory:
cItem = get_full_item_list()[item.name]
if cItem.type in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
unit_avail_upgrades[item.name] = []
unit_nb_upgrades[item.name] = 0
elif cItem.parent_item is not None:
if cItem.parent_item not in unit_avail_upgrades:
unit_avail_upgrades[cItem.parent_item] = [item]
unit_nb_upgrades[cItem.parent_item] = 1
else:
unit_avail_upgrades[cItem.parent_item].append(item)
unit_nb_upgrades[cItem.parent_item] += 1
# For those two categories, we count them but dont include them in removal
for item in locked_items + self.existing_items:
cItem = get_full_item_list()[item.name]
if cItem.type in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
unit_avail_upgrades[item.name] = []
unit_nb_upgrades[item.name] = 0
elif cItem.parent_item is not None:
if cItem.parent_item not in unit_avail_upgrades:
unit_nb_upgrades[cItem.parent_item] = 1
else:
unit_nb_upgrades[cItem.parent_item] += 1
# Making sure that the upgrades being removed is random
# Currently, only for combat shield vs Stabilizer Medpacks...
shuffled_unit_upgrade_list = list(unit_avail_upgrades.keys())
self.multiworld.random.shuffle(shuffled_unit_upgrade_list)
for unit in shuffled_unit_upgrade_list:
while (unit_nb_upgrades[unit] > maxUpgrad) \
and (len(unit_avail_upgrades[unit]) > 0):
itemCandidate = self.multiworld.random.choice(unit_avail_upgrades[unit])
_ = attempt_removal(itemCandidate)
# Whatever it succeed to remove the iventory or it fails and thus
# lock it, the upgrade is no longer available for removal
unit_avail_upgrades[unit].remove(itemCandidate)
unit_nb_upgrades[unit] -= 1
# Locking associated items for items that have already been placed when units_always_have_upgrades is on
if units_always_have_upgrades:
existing_items = set(self.existing_items[:] + locked_items)
while existing_items:
existing_item = existing_items.pop()
items_to_lock = self.cascade_removal_map.get(existing_item, [existing_item])
if get_full_item_list()[existing_item.name].type != "Upgrade":
# Don't process general upgrades, they may have been pre-locked per-level
for item in items_to_lock:
if item in inventory:
item_quantity = inventory.count(item)
# Unit upgrades, lock all levels
for _ in range(item_quantity):
inventory.remove(item)
if item not in locked_items:
# Lock all the associated items if not already locked
for _ in range(item_quantity):
locked_items.append(copy_item(item))
if item in existing_items:
existing_items.remove(item)
if self.min_units_per_structure > 0 and self.has_units_per_structure():
requirements.append(lambda state: state.has_units_per_structure())
# Determining if the full-size inventory can complete campaign
if not all(requirement(self) for requirement in requirements):
raise Exception("Too many items excluded - campaign is impossible to complete.")
while len(inventory) + len(locked_items) > inventory_size:
if len(inventory) == 0:
# There are more items than locations and all of them are already locked due to YAML or logic.
# Random items from locked ones will go to starting items
self.multiworld.random.shuffle(locked_items)
while len(locked_items) > inventory_size:
item: Item = locked_items.pop()
self.multiworld.push_precollected(item)
break
# Select random item from removable items
item = self.multiworld.random.choice(inventory)
# Cascade removals to associated items
if item in cascade_keys:
items_to_remove = self.cascade_removal_map[item]
transient_items = []
cascade_failure = False
while len(items_to_remove) > 0:
item_to_remove = items_to_remove.pop()
transient_items.append(item_to_remove)
if item_to_remove not in inventory:
if units_always_have_upgrades and item_to_remove in locked_items:
cascade_failure = True
break
else:
continue
success = attempt_removal(item_to_remove)
if not success and units_always_have_upgrades:
cascade_failure = True
transient_items += items_to_remove
break
# Lock all associated items if any of them cannot be removed on Units Always Have Upgrades
if cascade_failure:
for transient_item in transient_items:
if transient_item in inventory:
for _ in range(inventory.count(transient_item)):
inventory.remove(transient_item)
if transient_item not in locked_items:
for _ in range(get_item_quantity(transient_item, self.multiworld, self.player)):
locked_items.append(copy_item(transient_item))
if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing):
self.logical_inventory.add(transient_item.name)
else:
attempt_removal(item)
if not spider_mine_sources & self.logical_inventory:
inventory = [item for item in inventory if not item.name.endswith("(Spider Mine)")]
if not BARRACKS_UNITS & self.logical_inventory:
inventory = [item for item in inventory if
not (item.name.startswith("Progressive Infantry") or item.name == "Orbital Strike")]
if not FACTORY_UNITS & self.logical_inventory:
inventory = [item for item in inventory if not item.name.startswith("Progressive Vehicle")]
if not STARPORT_UNITS & self.logical_inventory:
inventory = [item for item in inventory if not item.name.startswith("Progressive Ship")]
# Cull finished, adding locked items back into inventory
inventory += locked_items
# Replacing empty space with generically useful items
replacement_items = [item for item in self.item_pool
if (item not in inventory
and item not in self.locked_items
and item.name in second_pass_placeable_items)]
self.multiworld.random.shuffle(replacement_items)
while len(inventory) < inventory_size and len(replacement_items) > 0:
item = replacement_items.pop()
inventory.append(item)
return inventory
def _read_logic(self):
self._sc2wol_has_common_unit = lambda world, player: SC2WoLLogic._sc2wol_has_common_unit(self, world, player)
self._sc2wol_has_air = lambda world, player: SC2WoLLogic._sc2wol_has_air(self, world, player)
self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player)
self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player)
self._sc2wol_has_competent_ground_to_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_ground_to_air(self, world, player)
self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player)
self._sc2wol_defense_rating = lambda world, player, zerg_enemy, air_enemy=False: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy, air_enemy)
self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player)
self._sc2wol_has_train_killers = lambda world, player: SC2WoLLogic._sc2wol_has_train_killers(self, world, player)
self._sc2wol_able_to_rescue = lambda world, player: SC2WoLLogic._sc2wol_able_to_rescue(self, world, player)
self._sc2wol_beats_protoss_deathball = lambda world, player: SC2WoLLogic._sc2wol_beats_protoss_deathball(self, world, player)
self._sc2wol_survives_rip_field = lambda world, player: SC2WoLLogic._sc2wol_survives_rip_field(self, world, player)
self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player)
self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player)
self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player)
self._sc2wol_welcome_to_the_jungle_requirement = lambda world, player: SC2WoLLogic._sc2wol_welcome_to_the_jungle_requirement(self, world, player)
self._sc2wol_can_respond_to_colony_infestations = lambda world, player: SC2WoLLogic._sc2wol_can_respond_to_colony_infestations(self, world, player)
self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player)
def __init__(self, multiworld: MultiWorld, player: int,
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item],
has_protoss: bool):
self.multiworld = multiworld
self.player = player
self.logical_inventory = set()
self.locked_items = locked_items[:]
self.existing_items = existing_items
self._read_logic()
# Initial filter of item pool
self.item_pool = []
item_quantities: dict[str, int] = dict()
# Inventory restrictiveness based on number of missions with checks
mission_order_type = get_option_value(self.multiworld, self.player, "mission_order")
mission_count = len(mission_orders[mission_order_type]) - 1
self.min_units_per_structure = int(mission_count / 7)
min_upgrades = 1 if mission_count < 10 else 2
for item in item_pool:
item_info = get_full_item_list()[item.name]
if item_info.type == "Upgrade":
# Locking upgrades based on mission duration
if item.name not in item_quantities:
item_quantities[item.name] = 0
item_quantities[item.name] += 1
if item_quantities[item.name] < min_upgrades:
self.locked_items.append(item)
else:
self.item_pool.append(item)
elif item_info.type == "Goal":
locked_items.append(item)
elif item_info.type != "Protoss" or has_protoss:
self.item_pool.append(item)
self.cascade_removal_map: Dict[Item, List[Item]] = dict()
for item in self.item_pool + locked_items + existing_items:
if item.name in UPGRADABLE_ITEMS:
upgrades = get_item_upgrades(self.item_pool, item)
associated_items = [*upgrades, item]
self.cascade_removal_map[item] = associated_items
if get_option_value(multiworld, player, "units_always_have_upgrades"):
for upgrade in upgrades:
self.cascade_removal_map[upgrade] = associated_items
def filter_items(multiworld: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo], location_cache: List[Location],
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item]) -> List[Item]:
"""
Returns a semi-randomly pruned set of items based on number of available locations.
The returned inventory must be capable of logically accessing every location in the world.
"""
open_locations = [location for location in location_cache if location.item is None]
inventory_size = len(open_locations)
has_protoss = bool(PROTOSS_REGIONS.intersection(mission_req_table.keys()))
mission_requirements = [location.access_rule for location in location_cache]
valid_inventory = ValidInventory(multiworld, player, item_pool, existing_items, locked_items, has_protoss)
valid_items = valid_inventory.generate_reduced_inventory(inventory_size, mission_requirements)
return valid_items

View File

@ -1,313 +0,0 @@
from typing import List, Set, Dict, Tuple, Optional, Callable
from BaseClasses import MultiWorld, Region, Entrance, Location
from .Locations import LocationData
from .Options import get_option_value, MissionOrder
from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, \
MissionPools, vanilla_shuffle_order
from .PoolFilter import filter_missions
PROPHECY_CHAIN_MISSION_COUNT = 4
VANILLA_SHUFFLED_FIRST_PROPHECY_MISSION = 21
def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\
-> Tuple[Dict[str, MissionInfo], int, str]:
locations_per_region = get_locations_per_region(locations)
mission_order_type = get_option_value(multiworld, player, "mission_order")
mission_order = mission_orders[mission_order_type]
mission_pools = filter_missions(multiworld, player)
regions = [create_region(multiworld, player, locations_per_region, location_cache, "Menu")]
names: Dict[str, int] = {}
if mission_order_type == MissionOrder.option_vanilla:
# Generating all regions and locations
for region_name in vanilla_mission_req_table.keys():
regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name))
multiworld.regions += regions
connect(multiworld, player, names, 'Menu', 'Liberation Day'),
connect(multiworld, player, names, 'Liberation Day', 'The Outlaws',
lambda state: state.has("Beat Liberation Day", player)),
connect(multiworld, player, names, 'The Outlaws', 'Zero Hour',
lambda state: state.has("Beat The Outlaws", player)),
connect(multiworld, player, names, 'Zero Hour', 'Evacuation',
lambda state: state.has("Beat Zero Hour", player)),
connect(multiworld, player, names, 'Evacuation', 'Outbreak',
lambda state: state.has("Beat Evacuation", player)),
connect(multiworld, player, names, "Outbreak", "Safe Haven",
lambda state: state._sc2wol_cleared_missions(multiworld, player, 7) and
state.has("Beat Outbreak", player)),
connect(multiworld, player, names, "Outbreak", "Haven's Fall",
lambda state: state._sc2wol_cleared_missions(multiworld, player, 7) and
state.has("Beat Outbreak", player)),
connect(multiworld, player, names, 'Zero Hour', 'Smash and Grab',
lambda state: state.has("Beat Zero Hour", player)),
connect(multiworld, player, names, 'Smash and Grab', 'The Dig',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
state.has("Beat Smash and Grab", player)),
connect(multiworld, player, names, 'The Dig', 'The Moebius Factor',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 11) and
state.has("Beat The Dig", player)),
connect(multiworld, player, names, 'The Moebius Factor', 'Supernova',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 14) and
state.has("Beat The Moebius Factor", player)),
connect(multiworld, player, names, 'Supernova', 'Maw of the Void',
lambda state: state.has("Beat Supernova", player)),
connect(multiworld, player, names, 'Zero Hour', "Devil's Playground",
lambda state: state._sc2wol_cleared_missions(multiworld, player, 4) and
state.has("Beat Zero Hour", player)),
connect(multiworld, player, names, "Devil's Playground", 'Welcome to the Jungle',
lambda state: state.has("Beat Devil's Playground", player)),
connect(multiworld, player, names, "Welcome to the Jungle", 'Breakout',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
state.has("Beat Welcome to the Jungle", player)),
connect(multiworld, player, names, "Welcome to the Jungle", 'Ghost of a Chance',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
state.has("Beat Welcome to the Jungle", player)),
connect(multiworld, player, names, "Zero Hour", 'The Great Train Robbery',
lambda state: state._sc2wol_cleared_missions(multiworld, player, 6) and
state.has("Beat Zero Hour", player)),
connect(multiworld, player, names, 'The Great Train Robbery', 'Cutthroat',
lambda state: state.has("Beat The Great Train Robbery", player)),
connect(multiworld, player, names, 'Cutthroat', 'Engine of Destruction',
lambda state: state.has("Beat Cutthroat", player)),
connect(multiworld, player, names, 'Engine of Destruction', 'Media Blitz',
lambda state: state.has("Beat Engine of Destruction", player)),
connect(multiworld, player, names, 'Media Blitz', 'Piercing the Shroud',
lambda state: state.has("Beat Media Blitz", player)),
connect(multiworld, player, names, 'The Dig', 'Whispers of Doom',
lambda state: state.has("Beat The Dig", player)),
connect(multiworld, player, names, 'Whispers of Doom', 'A Sinister Turn',
lambda state: state.has("Beat Whispers of Doom", player)),
connect(multiworld, player, names, 'A Sinister Turn', 'Echoes of the Future',
lambda state: state.has("Beat A Sinister Turn", player)),
connect(multiworld, player, names, 'Echoes of the Future', 'In Utter Darkness',
lambda state: state.has("Beat Echoes of the Future", player)),
connect(multiworld, player, names, 'Maw of the Void', 'Gates of Hell',
lambda state: state.has("Beat Maw of the Void", player)),
connect(multiworld, player, names, 'Gates of Hell', 'Belly of the Beast',
lambda state: state.has("Beat Gates of Hell", player)),
connect(multiworld, player, names, 'Gates of Hell', 'Shatter the Sky',
lambda state: state.has("Beat Gates of Hell", player)),
connect(multiworld, player, names, 'Gates of Hell', 'All-In',
lambda state: state.has('Beat Gates of Hell', player) and (
state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player)))
return vanilla_mission_req_table, 29, 'All-In: Victory'
else:
missions = []
remove_prophecy = mission_order_type == 1 and not get_option_value(multiworld, player, "shuffle_protoss")
final_mission = mission_pools[MissionPools.FINAL][0]
# Determining if missions must be removed
mission_pool_size = sum(len(mission_pool) for mission_pool in mission_pools.values())
removals = len(mission_order) - mission_pool_size
# Removing entire Prophecy chain on vanilla shuffled when not shuffling protoss
if remove_prophecy:
removals -= PROPHECY_CHAIN_MISSION_COUNT
# Initial fill out of mission list and marking all-in mission
for mission in mission_order:
# Removing extra missions if mission pool is too small
# Also handle lower removal priority than Prophecy
if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy \
or (remove_prophecy and mission_order_type == MissionOrder.option_vanilla_shuffled
and mission.removal_priority > vanilla_shuffle_order[
VANILLA_SHUFFLED_FIRST_PROPHECY_MISSION].removal_priority
and 0 < mission.removal_priority <= removals + PROPHECY_CHAIN_MISSION_COUNT):
missions.append(None)
elif mission.type == MissionPools.FINAL:
missions.append(final_mission)
else:
missions.append(mission.type)
no_build_slots = []
easy_slots = []
medium_slots = []
hard_slots = []
# Search through missions to find slots needed to fill
for i in range(len(missions)):
if missions[i] is None:
continue
if missions[i] == MissionPools.STARTER:
no_build_slots.append(i)
elif missions[i] == MissionPools.EASY:
easy_slots.append(i)
elif missions[i] == MissionPools.MEDIUM:
medium_slots.append(i)
elif missions[i] == MissionPools.HARD:
hard_slots.append(i)
# Add no_build missions to the pool and fill in no_build slots
missions_to_add = mission_pools[MissionPools.STARTER]
if len(no_build_slots) > len(missions_to_add):
raise Exception("There are no valid No-Build missions. Please exclude fewer missions.")
for slot in no_build_slots:
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Add easy missions into pool and fill in easy slots
missions_to_add = missions_to_add + mission_pools[MissionPools.EASY]
if len(easy_slots) > len(missions_to_add):
raise Exception("There are not enough Easy missions to fill the campaign. Please exclude fewer missions.")
for slot in easy_slots:
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Add medium missions into pool and fill in medium slots
missions_to_add = missions_to_add + mission_pools[MissionPools.MEDIUM]
if len(medium_slots) > len(missions_to_add):
raise Exception("There are not enough Easy and Medium missions to fill the campaign. Please exclude fewer missions.")
for slot in medium_slots:
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Add hard missions into pool and fill in hard slots
missions_to_add = missions_to_add + mission_pools[MissionPools.HARD]
if len(hard_slots) > len(missions_to_add):
raise Exception("There are not enough missions to fill the campaign. Please exclude fewer missions.")
for slot in hard_slots:
filler = multiworld.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler)
# Generating regions and locations from selected missions
for region_name in missions:
regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name))
multiworld.regions += regions
# Mapping original mission slots to shifted mission slots when missions are removed
slot_map = []
slot_offset = 0
for position, mission in enumerate(missions):
slot_map.append(position - slot_offset + 1)
if mission is None:
slot_offset += 1
# Loop through missions to create requirements table and connect regions
# TODO: Handle 'and' connections
mission_req_table = {}
def build_connection_rule(mission_names: List[str], missions_req: int) -> Callable:
if len(mission_names) > 1:
return lambda state: state.has_all({f"Beat {name}" for name in mission_names}, player) and \
state._sc2wol_cleared_missions(multiworld, player, missions_req)
else:
return lambda state: state.has(f"Beat {mission_names[0]}", player) and \
state._sc2wol_cleared_missions(multiworld, player, missions_req)
for i, mission in enumerate(missions):
if mission is None:
continue
connections = []
all_connections = []
for connection in mission_order[i].connect_to:
if connection == -1:
continue
while missions[connection] is None:
connection -= 1
all_connections.append(missions[connection])
for connection in mission_order[i].connect_to:
required_mission = missions[connection]
if connection == -1:
connect(multiworld, player, names, "Menu", mission)
else:
if required_mission is None and not mission_order[i].completion_critical: # Drop non-critical null slots
continue
while required_mission is None: # Substituting null slot with prior slot
connection -= 1
required_mission = missions[connection]
required_missions = [required_mission] if mission_order[i].or_requirements else all_connections
connect(multiworld, player, names, required_mission, mission,
build_connection_rule(required_missions, mission_order[i].number))
connections.append(slot_map[connection])
mission_req_table.update({mission: MissionInfo(
vanilla_mission_req_table[mission].id, connections, mission_order[i].category,
number=mission_order[i].number,
completion_critical=mission_order[i].completion_critical,
or_requirements=mission_order[i].or_requirements)})
final_mission_id = vanilla_mission_req_table[final_mission].id
# Changing the completion condition for alternate final missions into an event
if final_mission != 'All-In':
final_location = alt_final_mission_locations[final_mission]
# Final location should be near the end of the cache
for i in range(len(location_cache) - 1, -1, -1):
if location_cache[i].name == final_location:
location_cache[i].locked = True
location_cache[i].event = True
location_cache[i].address = None
break
else:
final_location = 'All-In: Victory'
return mission_req_table, final_mission_id, final_location
def create_location(player: int, location_data: LocationData, region: Region,
location_cache: List[Location]) -> Location:
location = Location(player, location_data.name, location_data.code, region)
location.access_rule = location_data.rule
if id is None:
location.event = True
location.locked = True
location_cache.append(location)
return location
def create_region(multiworld: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]],
location_cache: List[Location], name: str) -> Region:
region = Region(name, player, multiworld)
if name in locations_per_region:
for location_data in locations_per_region[name]:
location = create_location(player, location_data, region, location_cache)
region.locations.append(location)
return region
def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str,
rule: Optional[Callable] = None):
sourceRegion = world.get_region(source, player)
targetRegion = world.get_region(target, player)
if target not in used_names:
used_names[target] = 1
name = target
else:
used_names[target] += 1
name = target + (' ' * used_names[target])
connection = Entrance(player, name, sourceRegion)
if rule:
connection.access_rule = rule
sourceRegion.exits.append(connection)
connection.connect(targetRegion)
def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]:
per_region: Dict[str, List[LocationData]] = {}
for location in locations:
per_region.setdefault(location.region, []).append(location)
return per_region

View File

@ -1,16 +0,0 @@
<MissionLayout>:
rows: 1
<MissionCategory>:
cols: 1
padding: [10,5,10,5]
spacing: [0,5]
<MissionButton>:
text_size: self.size
markup: True
halign: 'center'
valign: 'middle'
padding: [5,0,5,0]
markup: True
outline_width: 1

View File

@ -1,324 +0,0 @@
import typing
from typing import List, Set, Tuple, Dict
from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
from worlds.AutoWorld import WebWorld, World
from .Items import StarcraftWoLItem, filler_items, item_name_groups, get_item_table, get_full_item_list, \
get_basic_units, ItemData, upgrade_included_names, progressive_if_nco
from .Locations import get_locations, LocationType
from .Regions import create_regions
from .Options import sc2wol_options, get_option_value, LocationInclusion
from .LogicMixin import SC2WoLLogic
from .PoolFilter import filter_missions, filter_items, get_item_upgrades
from .MissionTables import starting_mission_locations, MissionInfo
class Starcraft2WoLWebWorld(WebWorld):
setup = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Starcraft 2 randomizer connected to an Archipelago Multiworld",
"English",
"setup_en.md",
"setup/en",
["TheCondor"]
)
tutorials = [setup]
class SC2WoLWorld(World):
"""
StarCraft II: Wings of Liberty is a science fiction real-time strategy video game developed and published by Blizzard Entertainment.
Command Raynor's Raiders in collecting pieces of the Keystone in order to stop the zerg threat posed by the Queen of Blades.
"""
game = "Starcraft 2 Wings of Liberty"
web = Starcraft2WoLWebWorld()
data_version = 5
item_name_to_id = {name: data.code for name, data in get_full_item_list().items()}
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
option_definitions = sc2wol_options
item_name_groups = item_name_groups
locked_locations: typing.List[str]
location_cache: typing.List[Location]
mission_req_table = {}
final_mission_id: int
victory_item: str
required_client_version = 0, 4, 3
def __init__(self, multiworld: MultiWorld, player: int):
super(SC2WoLWorld, self).__init__(multiworld, player)
self.location_cache = []
self.locked_locations = []
def create_item(self, name: str) -> Item:
data = get_full_item_list()[name]
return StarcraftWoLItem(name, data.classification, data.code, self.player)
def create_regions(self):
self.mission_req_table, self.final_mission_id, self.victory_item = create_regions(
self.multiworld, self.player, get_locations(self.multiworld, self.player), self.location_cache
)
def create_items(self):
setup_events(self.player, self.locked_locations, self.location_cache)
excluded_items = get_excluded_items(self.multiworld, self.player)
starter_items = assign_starter_items(self.multiworld, self.player, excluded_items, self.locked_locations)
filter_locations(self.multiworld, self.player, self.locked_locations, self.location_cache)
pool = get_item_pool(self.multiworld, self.player, self.mission_req_table, starter_items, excluded_items, self.location_cache)
fill_item_pool_with_dummy_items(self, self.multiworld, self.player, self.locked_locations, self.location_cache, pool)
self.multiworld.itempool += pool
def set_rules(self):
self.multiworld.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player)
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(filler_items)
def fill_slot_data(self):
slot_data = {}
for option_name in sc2wol_options:
option = getattr(self.multiworld, option_name)[self.player]
if type(option.value) in {str, int}:
slot_data[option_name] = int(option.value)
slot_req_table = {}
for mission in self.mission_req_table:
slot_req_table[mission] = self.mission_req_table[mission]._asdict()
slot_data["mission_req"] = slot_req_table
slot_data["final_mission"] = self.final_mission_id
return slot_data
def setup_events(player: int, locked_locations: typing.List[str], location_cache: typing.List[Location]):
for location in location_cache:
if location.address is None:
item = Item(location.name, ItemClassification.progression, None, player)
locked_locations.append(location.name)
location.place_locked_item(item)
def get_excluded_items(multiworld: MultiWorld, player: int) -> Set[str]:
excluded_items: Set[str] = set()
for item in multiworld.precollected_items[player]:
excluded_items.add(item.name)
excluded_items_option = getattr(multiworld, 'excluded_items', [])
excluded_items.update(excluded_items_option[player].value)
return excluded_items
def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]) -> List[Item]:
non_local_items = multiworld.non_local_items[player].value
if get_option_value(multiworld, player, "early_unit"):
local_basic_unit = sorted(item for item in get_basic_units(multiworld, player) if item not in non_local_items and item not in excluded_items)
if not local_basic_unit:
raise Exception("At least one basic unit must be local")
# The first world should also be the starting world
first_mission = list(multiworld.worlds[player].mission_req_table)[0]
if first_mission in starting_mission_locations:
first_location = starting_mission_locations[first_mission]
elif first_mission == "In Utter Darkness":
first_location = first_mission + ": Defeat"
else:
first_location = first_mission + ": Victory"
return [assign_starter_item(multiworld, player, excluded_items, locked_locations, first_location, local_basic_unit)]
else:
return []
def assign_starter_item(multiworld: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
location: str, item_list: Tuple[str, ...]) -> Item:
item_name = multiworld.random.choice(item_list)
excluded_items.add(item_name)
item = create_item_with_correct_settings(player, item_name)
multiworld.get_location(location, player).place_locked_item(item)
locked_locations.append(location)
return item
def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo],
starter_items: List[Item], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
pool: List[Item] = []
# For the future: goal items like Artifact Shards go here
locked_items = []
# YAML items
yaml_locked_items = get_option_value(multiworld, player, 'locked_items')
# Adjust generic upgrade availability based on options
include_upgrades = get_option_value(multiworld, player, 'generic_upgrade_missions') == 0
upgrade_items = get_option_value(multiworld, player, 'generic_upgrade_items')
# Include items from outside Wings of Liberty
item_sets = {'wol'}
if get_option_value(multiworld, player, 'nco_items'):
item_sets.add('nco')
if get_option_value(multiworld, player, 'bw_items'):
item_sets.add('bw')
if get_option_value(multiworld, player, 'ext_items'):
item_sets.add('ext')
def allowed_quantity(name: str, data: ItemData) -> int:
if name in excluded_items \
or data.type == "Upgrade" and (not include_upgrades or name not in upgrade_included_names[upgrade_items]) \
or not data.origin.intersection(item_sets):
return 0
elif name in progressive_if_nco and 'nco' not in item_sets:
return 1
else:
return data.quantity
for name, data in get_item_table(multiworld, player).items():
for i in range(allowed_quantity(name, data)):
item = create_item_with_correct_settings(player, name)
if name in yaml_locked_items:
locked_items.append(item)
else:
pool.append(item)
existing_items = starter_items + [item for item in multiworld.precollected_items[player]]
existing_names = [item.name for item in existing_items]
# Check the parent item integrity, exclude items
pool[:] = [item for item in pool if pool_contains_parent(item, pool + locked_items + existing_items)]
# Removing upgrades for excluded items
for item_name in excluded_items:
if item_name in existing_names:
continue
invalid_upgrades = get_item_upgrades(pool, item_name)
for invalid_upgrade in invalid_upgrades:
pool.remove(invalid_upgrade)
filtered_pool = filter_items(multiworld, player, mission_req_table, location_cache, pool, existing_items, locked_items)
return filtered_pool
def fill_item_pool_with_dummy_items(self: SC2WoLWorld, multiworld: MultiWorld, player: int, locked_locations: List[str],
location_cache: List[Location], pool: List[Item]):
for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
item = create_item_with_correct_settings(player, self.get_filler_item_name())
pool.append(item)
def create_item_with_correct_settings(player: int, name: str) -> Item:
data = get_full_item_list()[name]
item = Item(name, data.classification, data.code, player)
return item
def pool_contains_parent(item: Item, pool: [Item]):
item_data = get_full_item_list().get(item.name)
if item_data.parent_item is None:
# The item has not associated parent, the item is valid
return True
parent_item = item_data.parent_item
# Check if the pool contains the parent item
return parent_item in [pool_item.name for pool_item in pool]
def filter_locations(multiworld: MultiWorld, player, locked_locations: List[str], location_cache: List[Location]):
"""
Filters the locations in the world using a trash or Nothing item
:param multiworld:
:param player:
:param locked_locations:
:param location_cache:
:return:
"""
open_locations = [location for location in location_cache if location.item is None]
plando_locations = get_plando_locations(multiworld, player)
mission_progress_locations = get_option_value(multiworld, player, "mission_progress_locations")
bonus_locations = get_option_value(multiworld, player, "bonus_locations")
challenge_locations = get_option_value(multiworld, player, "challenge_locations")
optional_boss_locations = get_option_value(multiworld, player, "optional_boss_locations")
location_data = get_locations(multiworld, player)
for location in open_locations:
# Go through the locations that aren't locked yet (early unit, etc)
if location.name not in plando_locations:
# The location is not plando'd
sc2_location = [sc2_location for sc2_location in location_data if sc2_location.name == location.name][0]
location_type = sc2_location.type
if location_type == LocationType.MISSION_PROGRESS \
and mission_progress_locations != LocationInclusion.option_enabled:
item_name = get_exclusion_item(multiworld, mission_progress_locations)
place_exclusion_item(item_name, location, locked_locations, player)
if location_type == LocationType.BONUS \
and bonus_locations != LocationInclusion.option_enabled:
item_name = get_exclusion_item(multiworld, bonus_locations)
place_exclusion_item(item_name, location, locked_locations, player)
if location_type == LocationType.CHALLENGE \
and challenge_locations != LocationInclusion.option_enabled:
item_name = get_exclusion_item(multiworld, challenge_locations)
place_exclusion_item(item_name, location, locked_locations, player)
if location_type == LocationType.OPTIONAL_BOSS \
and optional_boss_locations != LocationInclusion.option_enabled:
item_name = get_exclusion_item(multiworld, optional_boss_locations)
place_exclusion_item(item_name, location, locked_locations, player)
def place_exclusion_item(item_name, location, locked_locations, player):
item = create_item_with_correct_settings(player, item_name)
location.place_locked_item(item)
locked_locations.append(location.name)
def get_exclusion_item(multiworld: MultiWorld, option) -> str:
"""
Gets the exclusion item according to settings (trash/nothing)
:param multiworld:
:param option:
:return: Item used for location exclusion
"""
if option == LocationInclusion.option_nothing:
return "Nothing"
elif option == LocationInclusion.option_trash:
index = multiworld.random.randint(0, len(filler_items) - 1)
return filler_items[index]
raise Exception(f"Unsupported option type: {option}")
def get_plando_locations(multiworld: MultiWorld, player) -> List[str]:
"""
:param multiworld:
:param player:
:return: A list of locations affected by a plando in a world
"""
plando_locations = []
for plando_setting in multiworld.plando_items[player]:
plando_locations += plando_setting.get("locations", [])
plando_setting_location = plando_setting.get("location", None)
if plando_setting_location is not None:
plando_locations.append(plando_setting_location)
return plando_locations

View File

@ -1,54 +0,0 @@
# Starcraft 2 Wings of Liberty
## What does randomization do to this game?
The following unlocks are randomized as items:
1. Your ability to build any non-worker unit (including Marines!).
2. Your ability to upgrade infantry weapons, infantry armor, vehicle weapons, etc.
3. All armory upgrades
4. All laboratory upgrades
5. All mercenaries
6. Small boosts to your starting mineral and vespene gas totals on each mission
You find items by making progress in bonus objectives (like by rescuing allies in 'Zero Hour') and by completing
missions. When you receive items, they will immediately become available, even during a mission, and you will be
notified via a text box in the top-right corner of the game screen. (The text client for StarCraft 2 also records all
items in all worlds.)
Missions are launched only through the text client. The Hyperion is never visited. Additionally, credits are not used.
## What is the goal of this game when randomized?
The goal is to beat the final mission: 'All In'. The config file determines which variant you must complete.
## What non-randomized changes are there from vanilla Starcraft 2?
1. Some missions have more vespene geysers available to allow a wider variety of units.
2. Starports no longer require Factories in order to be built.
3. In 'A Sinister Turn' and 'Echoes of the Future', you can research Protoss air weapon/armor upgrades.
## Which of my items can be in another player's world?
By default, any of StarCraft 2's items (specified above) can be in another player's world. See the
[Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en)
for more information on how to change this.
## Unique Local Commands
The following commands are only available when using the Starcraft 2 Client to play with Archipelago.
- `/difficulty [difficulty]` Overrides the difficulty set for the world.
- Options: casual, normal, hard, brutal
- `/game_speed [game_speed]` Overrides the game speed for the world
- Options: default, slower, slow, normal, fast, faster
- `/color [color]` Changes your color (Currently has no effect)
- Options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown,
lightgreen, darkgrey, pink, rainbow, random, default
- `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one
player can play the next mission in a chain the other player is doing.
- `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided
- `/available` Get what missions are currently available to play
- `/unfinished` Get what missions are currently available to play and have not had all locations checked
- `/set_path [path]` Menually set the SC2 install directory (if the automatic detection fails)
- `/download_data` Download the most recent release of the necassry files for playing SC2 with Archipelago. Will
overwrite existing files